Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions examples/servers/typescript/sep-2106-compliant-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#!/usr/bin/env node

/**
* SEP-2106 Compliant Reference Server (structuredContent wire-shape)
*
* A bare-bones Streamable-HTTP MCP server, implemented in raw Express, that
* speaks the SEP-2106 wire format for the parts SEP-1613 + #295 don't reach:
*
* - `tools/list` advertises a tool with `outputSchema.type === "array"`
* at the root, and another with `outputSchema.type === "number"` at the
* root (i.e. neither wrapped in `{type:"object"}`).
* - `tools/call` on those tools returns a JSON array (resp. raw number)
* directly in `structuredContent` — exactly the shape SEP-2106 permits
* and the MCP SDK Server (as of the version pinned in this repo) refuses
* to emit because `CallToolResultSchema.structuredContent` is still typed
* `Record<string, unknown>`.
*
* Why no SDK: until the SDK ships SEP-2106's widening of
* `CallToolResultSchema.structuredContent` to `unknown`, the SDK Server
* validates outgoing responses and rejects array/primitive structuredContent
* with a JSON-RPC -32602. Writing JSON-RPC directly is the only way to
* demonstrate a SEP-2106-compliant server today and gives a clean positive-
* test target for the `sep-2106-structured-content` scenario.
*
* Used by `src/scenarios/server/negative.test.ts` as the positive case for
* `Sep2106StructuredContentScenario`; all of its checks should succeed
* against this server.
*/

import express from 'express';

const PROTOCOL_VERSION = 'DRAFT-2026-v1';

const FORECAST_PAYLOAD = [
{ hour: '09:00', temp: 68, conditions: 'sunny' },
{ hour: '10:00', temp: 72, conditions: 'partly cloudy' },
{ hour: '11:00', temp: 75, conditions: 'cloudy' }
];

const COUNT_PAYLOAD = 42;

const ARRAY_OUTPUT_SCHEMA = {
type: 'array',
items: {
type: 'object',
properties: {
hour: { type: 'string' },
temp: { type: 'number' },
conditions: { type: 'string' }
},
required: ['hour', 'temp', 'conditions']
}
};

const PRIMITIVE_OUTPUT_SCHEMA = { type: 'number' };

const TOOLS = [
{
name: 'sep_2106_array_output_tool',
description:
'Returns an array of hourly forecasts directly in structuredContent (SEP-2106 wire shape)',
inputSchema: { type: 'object', properties: {} },
outputSchema: ARRAY_OUTPUT_SCHEMA
},
{
name: 'sep_2106_primitive_output_tool',
description:
'Returns a raw number directly in structuredContent (SEP-2106 wire shape)',
inputSchema: { type: 'object', properties: {} },
outputSchema: PRIMITIVE_OUTPUT_SCHEMA
}
];

function handleRequest(body: any): { status: number; payload: unknown } {
const { method, id, params } = body ?? {};

if (method === 'initialize') {
return {
status: 200,
payload: {
jsonrpc: '2.0',
id,
result: {
protocolVersion: PROTOCOL_VERSION,
serverInfo: { name: 'sep-2106-compliant', version: '1.0.0' },
capabilities: { tools: {} }
}
}
};
}

if (method === 'notifications/initialized') {
return { status: 202, payload: null };
}

if (method === 'tools/list') {
return {
status: 200,
payload: { jsonrpc: '2.0', id, result: { tools: TOOLS } }
};
}

if (method === 'tools/call') {
const name = params?.name;
if (name === 'sep_2106_array_output_tool') {
return {
status: 200,
payload: {
jsonrpc: '2.0',
id,
result: {
content: [{ type: 'text', text: JSON.stringify(FORECAST_PAYLOAD) }],
structuredContent: FORECAST_PAYLOAD
}
}
};
}
if (name === 'sep_2106_primitive_output_tool') {
return {
status: 200,
payload: {
jsonrpc: '2.0',
id,
result: {
content: [{ type: 'text', text: String(COUNT_PAYLOAD) }],
structuredContent: COUNT_PAYLOAD
}
}
};
}
return {
status: 200,
payload: {
jsonrpc: '2.0',
id,
error: { code: -32602, message: `Unknown tool: ${name}` }
}
};
}

return {
status: 200,
payload: {
jsonrpc: '2.0',
id,
error: { code: -32601, message: `Method not found: ${method}` }
}
};
}

const app = express();
app.use(express.json());

app.post('/mcp', (req, res) => {
const { status, payload } = handleRequest(req.body);
if (payload === null) {
res.status(status).end();
return;
}
res.status(status).json(payload);
});

const PORT = parseInt(process.env.PORT || '3009', 10);
app.listen(PORT, '127.0.0.1', () => {
console.log(
`SEP-2106 compliant test server running on http://localhost:${PORT}/mcp`
);
});
11 changes: 11 additions & 0 deletions src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
} from './server/tools';

import { JsonSchema2020_12Scenario } from './server/json-schema-2020-12';
import { Sep2106StructuredContentScenario } from './server/sep-2106-structured-content';

import { ElicitationDefaultsScenario } from './server/elicitation-defaults';
import { ElicitationEnumsScenario } from './server/elicitation-enums';
Expand Down Expand Up @@ -114,6 +115,13 @@ const pendingClientScenariosList: ClientScenario[] = [
// $schema, $defs, and additionalProperties fields in tool schemas.
new JsonSchema2020_12Scenario(),

// SEP-2106: structuredContent wire shape (array / primitive at root).
// Pending until the SDK widens CallToolResultSchema.structuredContent to
// `unknown`; until then the in-repo everything-server cannot emit non-object
// structuredContent (the SDK Server's outgoing validator rejects it). The
// compliant reference target is examples/servers/typescript/sep-2106-compliant-server.ts.
new Sep2106StructuredContentScenario(),

// On hold until server-side SSE improvements are made
// https://github.com/modelcontextprotocol/typescript-sdk/pull/1129
new ServerSSEPollingScenario(),
Expand Down Expand Up @@ -152,6 +160,9 @@ const allClientScenariosList: ClientScenario[] = [
// JSON Schema 2020-12 support (SEP-1613)
new JsonSchema2020_12Scenario(),

// SEP-2106 structuredContent wire shape (pending)
new Sep2106StructuredContentScenario(),

// Elicitation scenarios (SEP-1034)
new ElicitationDefaultsScenario(),

Expand Down
53 changes: 53 additions & 0 deletions src/scenarios/server/negative.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
JsonSchema2020_12Scenario,
sep2106KeywordCheckStatus
} from './json-schema-2020-12';
import { Sep2106StructuredContentScenario } from './sep-2106-structured-content';
import { DRAFT_PROTOCOL_VERSION, LATEST_SPEC_VERSION } from '../../types';

function startServer(scriptPath: string, port: number): Promise<ChildProcess> {
Expand Down Expand Up @@ -224,4 +225,56 @@ describe('Server scenario negative tests', () => {
expect(sep2106KeywordCheckStatus(false, undefined)).toBe('SKIPPED');
});
});

describe('sep-2106-structured-content (positive against compliant server)', () => {
let serverProcess: ChildProcess | null = null;
const PORT = 3009;

beforeAll(async () => {
serverProcess = await startServer(
path.join(
process.cwd(),
'examples/servers/typescript/sep-2106-compliant-server.ts'
),
PORT
);
}, 35000);

afterAll(async () => {
await stopServer(serverProcess);
});

it('emits all SUCCESS against the SEP-2106-compliant reference server', async () => {
const scenario = new Sep2106StructuredContentScenario();
const checks = await scenario.run(`http://localhost:${PORT}/mcp`);

// Every check should be SUCCESS — no FAILURE, no WARNING.
const failures = checks.filter((c) => c.status === 'FAILURE');
const warnings = checks.filter((c) => c.status === 'WARNING');
expect(
failures,
`unexpected failures: ${failures.map((f) => `${f.id}: ${f.errorMessage}`).join('; ')}`
).toHaveLength(0);
expect(
warnings,
`unexpected warnings: ${warnings.map((w) => `${w.id}: ${w.errorMessage}`).join('; ')}`
).toHaveLength(0);

// Sanity: all expected check IDs are present.
const expectedIds = [
'sep-2106-array-output-tool-found',
'sep-2106-array-output-schema-preserved',
'sep-2106-array-structured-content',
'sep-2106-primitive-output-tool-found',
'sep-2106-primitive-output-schema-preserved',
'sep-2106-primitive-structured-content'
];
for (const id of expectedIds) {
expect(
checks.find((c) => c.id === id),
`${id} missing`
).toBeDefined();
}
}, 15000);
});
});
Loading
Loading