Skip to content

Commit 2ab3c54

Browse files
committed
Docs updates, including composable test server design.
1 parent 4253d59 commit 2ab3c54

2 files changed

Lines changed: 340 additions & 0 deletions

File tree

docs/composable-test-server.md

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
# Config-Driven Composable MCP Server: Design Document
2+
3+
## Overview
4+
5+
The Inspector core package has **composable test MCP servers** (`composable-test-server.ts`, `test-server-fixtures.ts`) that allow creating an MCP server with a specific shape—tools, resources, prompts, capabilities—by passing a `ServerConfig` object. These are used today only in tests; they are composed in code.
6+
7+
This document proposes a **runtime MCP server** that reads a configuration file (JSON or YAML) to compose the server at startup. You could run it from MCP Inspector or any MCP client without writing code, e.g.:
8+
9+
```bash
10+
mcp-composable-server --config ./my-server-config.json
11+
# or
12+
mcp-composable-server --config ./my-server-config.yaml
13+
```
14+
15+
Use cases:
16+
17+
- Manually testing MCP Inspector (or other clients) with a specific server shape
18+
- Demos and documentation examples
19+
- Local development with a known-good server configuration
20+
21+
---
22+
23+
## Relationship to the "Everything" Server
24+
25+
The **@modelcontextprotocol/server-everything** package is the standard test-bench server for MCP clients. New features are typically added there first so they can be exercised in Inspector. It provides a fixed, comprehensive server that exposes many protocol features: echo/add tools, long-running operations, sampling, elicitation, annotated messages, 100 resources with subscription updates, prompts, logging, and more.
26+
27+
The contemplated config-driven composable server is **complementary**, not a replacement. In some situations it has advantages:
28+
29+
| Situation | Composable server advantage | Everything server |
30+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
31+
| **Testing specific capability combinations** | Compose exactly the subset you need (e.g. tools only, resources only, tasks + resources). Isolate behavior without unrelated features. | Fixed "kitchen sink" shape. Harder to isolate a single capability. |
32+
| **Pagination testing** | `maxPageSize` configurable per list type (tools, resources, templates, prompts). Can test cursor behavior with small page sizes (e.g. 2 or 3) without a huge catalog. | Fixed resource/prompt counts. No configurable pagination. |
33+
| **Controlled, predictable behavior** | No random log messages or timers. Responses are deterministic. Easier to reproduce bugs and write test cases. | Random log messages every 15 seconds, subscription updates every 5 seconds. |
34+
| **listChanged / subscriptions** | Enable or disable `listChanged` per list type. Test client handling of list changes without background noise. `subscriptions` toggle for resource updates. | Fixed behavior. Background updates may mask or confuse list-change tests. |
35+
| **Task variants** | Multiple task presets: immediate, progress, elicitation, sampling, optional vs required task support. Test task-related client behavior in isolation. | Has task-like behavior but in a fixed form. |
36+
| **Local, offline, no network** | Runs locally; config file and stdio/HTTP. No dependency on hosted services. | Hosted option exists but may have CORS/network constraints; local runs require npm install. |
37+
| **Rapid iteration on client features** | Swap config files to test different server shapes without code changes. E.g. `pagination-tools.json`, `tasks-only.json`, `list-changed-resources.json`. | Single fixed shape. New features require upstream changes to everything. |
38+
| **OAuth and auth testing** | Can enable OAuth with configurable static clients, DCR, CIMD. Test auth flows against a local server. | OAuth support exists; configurability may differ. |
39+
40+
**When to use Everything:** Broad coverage of protocol features, community standard, quick `npx` start. Best for "does the client work with a real MCP server?" and for validating that new features in everything are supported by Inspector.
41+
42+
**When to use the composable server:** Focused testing of pagination, list changes, tasks, capability subsets, or reproducible scenarios. Useful when debugging client behavior that depends on a specific server shape or when Everything doesn't yet support a feature you need to test.
43+
44+
---
45+
46+
## Current Architecture
47+
48+
### Core Components
49+
50+
1. **`createMcpServer(config: ServerConfig)`** (composable-test-server.ts)
51+
Takes `ServerConfig` and returns an `McpServer` (SDK). Handles all MCP capabilities, registration, and handlers.
52+
53+
2. **`ServerConfig`** (composable-test-server.ts)
54+
Configures:
55+
- `serverInfo`: name, version (Implementation)
56+
- `tools`, `resources`, `resourceTemplates`, `prompts`: arrays of definitions
57+
- Capabilities: `logging`, `listChanged`, `subscriptions`, `tasks`, `oauth`
58+
- Transport: `serverType` ("sse" | "streamable-http"), `port`
59+
- `maxPageSize` for pagination
60+
- `taskStore`, `taskMessageQueue` (advanced, optional)
61+
62+
3. **Test server fixtures** (test-server-fixtures.ts)
63+
Factory functions that return `ToolDefinition`, `ResourceDefinition`, `PromptDefinition`, `ResourceTemplateDefinition`:
64+
- **Tools**: echo, add, get-sum, collectSample, listRoots, collectElicitation, collectUrlElicitation, sendNotification, get-annotated-message, writeToStderr; addResource, removeResource, addTool, removeTool, addPrompt, removePrompt, updateResource; sendProgress; task tools (simpleTask, progressTask, elicitationTask, samplingTask, optionalTask, forbiddenTask, immediateReturnTask); numbered tools
65+
- **Resources**: architecture, test-cwd, test-env, test-argv; numbered resources
66+
- **Resource templates**: file, user; numbered templates
67+
- **Prompts**: simple-prompt, args-prompt; numbered prompts
68+
- **Presets**: `getDefaultServerConfig()`, `getTaskServerConfig()`
69+
70+
4. **Transports**
71+
- **test-server-stdio.ts**: `StdioServerTransport` — can run standalone with default config
72+
- **test-server-http.ts**: `StreamableHTTPServerTransport` or `SSEServerTransport` — used in tests
73+
74+
### Challenge: Handlers Are Functions
75+
76+
Tool, resource, and prompt definitions include **handler functions** (e.g. `ToolDefinition.handler`). These cannot be serialized in JSON/YAML. The config file must therefore reference **presets** (named fixtures) instead of defining handlers inline. The runtime resolves preset names to the actual definitions.
77+
78+
---
79+
80+
## Proposed Design
81+
82+
### 1. Config File Format
83+
84+
The config file mirrors `ServerConfig` but uses **preset references** instead of inline definitions. Supported formats: JSON and YAML.
85+
86+
**Preset reference format** (for tools, resources, prompts, resourceTemplates):
87+
88+
```json
89+
{
90+
"preset": "<preset-name>",
91+
"params": { ... }
92+
}
93+
```
94+
95+
- `preset` (required): name of a known preset
96+
- `params` (optional): parameters for that preset
97+
98+
**Example config file** (`example-server.json`):
99+
100+
```json
101+
{
102+
"serverInfo": {
103+
"name": "my-demo-server",
104+
"version": "1.0.0"
105+
},
106+
"tools": [
107+
{ "preset": "echo" },
108+
{ "preset": "add" },
109+
{ "preset": "numberedTools", "params": { "count": 5 } },
110+
{
111+
"preset": "simpleTask",
112+
"params": { "name": "slowTask", "delayMs": 3000 }
113+
}
114+
],
115+
"resources": [
116+
{ "preset": "architecture" },
117+
{ "preset": "test-cwd" },
118+
{ "preset": "numberedResources", "params": { "count": 3 } }
119+
],
120+
"resourceTemplates": [{ "preset": "file" }, { "preset": "user" }],
121+
"prompts": [{ "preset": "simple-prompt" }, { "preset": "args-prompt" }],
122+
"logging": true,
123+
"listChanged": {
124+
"tools": true,
125+
"resources": true,
126+
"prompts": true
127+
},
128+
"subscriptions": false,
129+
"tasks": { "list": true, "cancel": true },
130+
"transport": {
131+
"type": "stdio"
132+
}
133+
}
134+
```
135+
136+
For HTTP transport:
137+
138+
```json
139+
{
140+
"serverInfo": { "name": "http-demo", "version": "1.0.0" },
141+
"tools": [{ "preset": "echo" }],
142+
"transport": {
143+
"type": "streamable-http",
144+
"port": 3000
145+
}
146+
}
147+
```
148+
149+
---
150+
151+
### 2. Preset Registry
152+
153+
A **preset registry** maps preset names to factory functions that return definitions. The registry is populated with all fixtures from `test-server-fixtures.ts`.
154+
155+
| Preset Name | Type | Params | Notes |
156+
| ------------------------- | ------------------ | ------------------------------- | --------------------------------------------- |
157+
| echo | tool | none | Echo tool |
158+
| add | tool | none | Add two numbers |
159+
| get-sum | tool | none | Alias for add |
160+
| writeToStderr | tool | none | Writes message to stderr |
161+
| collectSample | tool | none | Sends sampling request to client |
162+
| listRoots | tool | none | Calls roots/list on client |
163+
| collectElicitation | tool | none | Sends form elicitation request |
164+
| collectUrlElicitation | tool | none | Sends URL elicitation request |
165+
| sendNotification | tool | none | Sends notification to client |
166+
| get-annotated-message | tool | none | Returns annotated message with optional image |
167+
| addResource | tool | none | Adds resource, sends list_changed |
168+
| removeResource | tool | none | Removes resource |
169+
| addTool | tool | none | Adds tool dynamically |
170+
| removeTool | tool | none | Removes tool |
171+
| addPrompt | tool | none | Adds prompt dynamically |
172+
| removePrompt | tool | none | Removes prompt |
173+
| updateResource | tool | none | Updates resource content |
174+
| sendProgress | tool | params: name? | Sends progress notifications |
175+
| numberedTools | tool[] | count | Creates N echo-like tools |
176+
| simpleTask | taskTool | name?, delayMs? | Task that completes after delay |
177+
| progressTask | taskTool | name?, delayMs?, progressUnits? | Task with progress |
178+
| elicitationTask | taskTool | name?, elicitationSchema? | Task requiring elicitation |
179+
| samplingTask | taskTool | name?, samplingText? | Task requiring sampling |
180+
| optionalTask | taskTool | name?, delayMs? | Task with optional task support |
181+
| forbiddenTask | tool | name?, delayMs? | Non-task tool (completes immediately) |
182+
| immediateReturnTask | tool | name?, delayMs? | Immediate return (no task) |
183+
| architecture | resource | none | Static architecture doc |
184+
| test-cwd | resource | none | Exposes process.cwd() |
185+
| test-env | resource | none | Exposes process.env |
186+
| test-argv | resource | none | Exposes process.argv |
187+
| numberedResources | resource[] | count | N static resources |
188+
| file | resourceTemplate | none | file:///{path} template |
189+
| user | resourceTemplate | none | user://{userId} template |
190+
| numberedResourceTemplates | resourceTemplate[] | count | N templates |
191+
| simple-prompt | prompt | none | Simple static prompt |
192+
| args-prompt | prompt | none | Prompt with city, state args |
193+
| numberedPrompts | prompt[] | count | N static prompts |
194+
195+
**Preset params** (where applicable):
196+
197+
- `numberedTools`, `numberedResources`, `numberedResourceTemplates`, `numberedPrompts`: `{ count: number }`
198+
- `simpleTask`, `progressTask`, etc.: `{ name?: string, delayMs?: number, progressUnits?: number, ... }` (see `TaskToolOptions`, `ImmediateToolOptions` in fixtures)
199+
- `sendProgress`: `{ name?: string }`
200+
201+
---
202+
203+
### 3. Config Schema
204+
205+
Top-level config structure:
206+
207+
```ts
208+
interface ConfigFile {
209+
serverInfo: {
210+
name: string;
211+
version: string;
212+
};
213+
tools?: Array<PresetRef | PresetRef[]>;
214+
resources?: PresetRef[];
215+
resourceTemplates?: PresetRef[];
216+
prompts?: PresetRef[];
217+
logging?: boolean;
218+
listChanged?: {
219+
tools?: boolean;
220+
resources?: boolean;
221+
prompts?: boolean;
222+
};
223+
subscriptions?: boolean;
224+
tasks?: {
225+
list?: boolean;
226+
cancel?: boolean;
227+
};
228+
maxPageSize?: {
229+
tools?: number;
230+
resources?: number;
231+
resourceTemplates?: number;
232+
prompts?: number;
233+
};
234+
transport: {
235+
type: "stdio" | "streamable-http" | "sse";
236+
port?: number; // For HTTP transports
237+
};
238+
}
239+
240+
interface PresetRef {
241+
preset: string;
242+
params?: Record<string, unknown>;
243+
}
244+
```
245+
246+
Arrays like `tools` can also accept arrays of preset refs (e.g. `numberedTools` expands to multiple tools).
247+
248+
---
249+
250+
### 4. Runtime Resolution
251+
252+
1. **Load config file** (JSON or YAML based on extension or explicit `--format`).
253+
2. **Resolve preset refs**: For each entry in tools, resources, resourceTemplates, prompts:
254+
- Look up preset in registry.
255+
- Call factory with `params` (or defaults).
256+
- Collect resulting definitions.
257+
3. **Build ServerConfig** from resolved definitions + top-level flags (logging, listChanged, tasks, etc.).
258+
4. **Create McpServer** via `createMcpServer(config)`.
259+
5. **Start transport**:
260+
- `stdio`: Connect `StdioServerTransport` (same pattern as test-server-stdio).
261+
- `streamable-http` or `sse`: Start HTTP server on `port` (or auto-assign), set up routes.
262+
263+
---
264+
265+
### 5. Implementation Plan
266+
267+
| Phase | Scope |
268+
| ----- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
269+
| 1 | Preset registry: map preset names to fixture factories. Expose from a new module (e.g. `core/test/preset-registry.ts`). |
270+
| 2 | Config loader: parse JSON/YAML, validate against schema (optional, or fail fast on unknown presets). |
271+
| 3 | Resolver: convert config file → `ServerConfig`. |
272+
| 4 | CLI entry point: `mcp-composable-server --config <path> [--transport stdio | http]`. Default transport from config. |
273+
| 5 | Package: Add `bin` in core (or a dedicated `cli-composable` package) so `npx` or npm script can run it. |
274+
275+
---
276+
277+
### 6. Placement and Packaging
278+
279+
**Option A: In core package**
280+
281+
- Add `core/bin/composable-server.ts` (or `.js` after build).
282+
- Add `"composable-server": "node build/test/composable-server.js"` to `bin` in `core/package.json`.
283+
- Config loader and preset registry live under `core/test/` or `core/config/`.
284+
285+
**Option B: Separate package**
286+
287+
- New package `@modelcontextprotocol/inspector-composable-server` (or `mcp-composable-server`).
288+
- Depends on `@modelcontextprotocol/inspector-core` for `createMcpServer` and fixtures.
289+
- Cleaner separation but more packaging overhead.
290+
291+
Recommendation: Start with **Option A** (in core) to reuse fixtures and `createMcpServer` directly. Move to a separate package later if needed.
292+
293+
---
294+
295+
### 7. Limitations and Future Work
296+
297+
1. **No custom handlers in config** — Only presets. Custom tools/resources require code or new presets.
298+
2. **OAuth** — OAuth config is complex (issuer URL, static clients, DCR, CIMD). Initial version can omit or support a minimal subset; expand later.
299+
3. **Elicitation/sampling schemas** — Task presets like `elicitationTask` accept `elicitationSchema`. In config, we could support a JSON Schema object; the resolver would convert to Zod or the SDK's schema format.
300+
4. **Completion callbacks** — Resource templates and prompts can have completion callbacks. Presets like `file` and `args-prompt` support them; config-driven mode would use defaults (e.g. no completion) unless we add preset params for static completion data.
301+
5. **YAML support** — Requires a YAML parser (e.g. `yaml`). Add as optional dependency.
302+
303+
---
304+
305+
### 8. Example Usage
306+
307+
```bash
308+
# Stdio transport (default for MCP Inspector stdio config)
309+
npx @modelcontextprotocol/inspector-core composable-server --config ./demo.json
310+
311+
# HTTP transport
312+
npx @modelcontextprotocol/inspector-core composable-server --config ./demo-http.json --port 3000
313+
```
314+
315+
**mcp.json** (MCP Inspector config) for stdio:
316+
317+
```json
318+
{
319+
"mcpServers": {
320+
"demo": {
321+
"command": "npx",
322+
"args": [
323+
"@modelcontextprotocol/inspector-core",
324+
"composable-server",
325+
"--config",
326+
"./demo.json"
327+
]
328+
}
329+
}
330+
}
331+
```
332+
333+
For HTTP, use a server URL instead of command/args.
334+
335+
---
336+
337+
## Summary
338+
339+
A config-driven composable MCP server would let users run a composed server from a JSON/YAML file without writing code. The design reuses `createMcpServer` and existing fixtures via a preset registry. Preset refs in config replace inline handler definitions. The runtime resolves presets, builds `ServerConfig`, and starts stdio or HTTP transport. Implementation can begin with a preset registry and config loader in core, then add a CLI entry point.

0 commit comments

Comments
 (0)