Skip to content
Open
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
83 changes: 67 additions & 16 deletions examples/clients/typescript/everything-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ import { JWT_BEARER_GRANT_TYPE } from '../../../src/scenarios/client/auth/helper
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { ClientConformanceContextSchema } from '../../../src/schemas/context.js';
import { DRAFT_PROTOCOL_VERSION } from '../../../src/types.js';
import {
classifyHttpFallbackResponse,
MODERN_PROBE_ID,
modernProbeRequestInit
} from '../../../src/version-compat.js';
import { STATELESS_SPEC_VERSIONS } from '../../../src/connection/select.js';
import {
auth,
Expand Down Expand Up @@ -155,6 +160,31 @@ async function statelessRequest(
// Basic scenarios (initialize, tools_call)
// ============================================================================

async function runLegacyBasicClient(serverUrl: string): Promise<void> {
const client = new Client(
{ name: 'test-client', version: '1.0.0' },
{ capabilities: {} }
);
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));

try {
await client.connect(transport);
logger.debug('Successfully connected to MCP server');

const list = await client.listTools();
logger.debug('Successfully listed tools');

const tool = list.tools[0];
if (tool) {
await client.callTool({ name: tool.name, arguments: { a: 2, b: 3 } });
logger.debug('Successfully called tool');
}
} finally {
await transport.close();
logger.debug('Connection closed successfully');
}
}

async function runBasicClient(serverUrl: string): Promise<void> {
if (USE_STATELESS_LIFECYCLE) {
logger.debug('Stateless lifecycle: calling tools/list + tools/call');
Expand All @@ -171,30 +201,51 @@ async function runBasicClient(serverUrl: string): Promise<void> {
return;
}

const client = new Client(
{ name: 'test-client', version: '1.0.0' },
{ capabilities: {} }
);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
await runLegacyBasicClient(serverUrl);
}

await client.connect(transport);
logger.debug('Successfully connected to MCP server');
const legacyOrigins = new Set<string>();

const list = await client.listTools();
logger.debug('Successfully listed tools');
async function runVersionBackcompatClient(serverUrl: string): Promise<void> {
const origin = new URL(serverUrl).origin;
const usedCachedLegacyDecision = legacyOrigins.has(origin);
if (!usedCachedLegacyDecision) {
const response = await fetch(serverUrl, modernProbeRequestInit());
const classification = await classifyHttpFallbackResponse(
response,
MODERN_PROBE_ID
);

const tool = list.tools[0];
if (tool) {
await client.callTool({ name: tool.name, arguments: { a: 2, b: 3 } });
logger.debug('Successfully called tool');
if (classification.kind === 'modern') {
throw new Error(
`Modern probe returned recognized modern error code ${classification.errorCode}`
);
}
if (classification.kind === 'unavailable') {
throw new Error(`Modern probe failed: ${classification.error}`);
}
if (classification.kind !== 'legacy-fallback') {
throw new Error(
`Expected legacy endpoint to reject the modern probe with HTTP 400, got ${classification.status}`
);
}
legacyOrigins.add(origin);
}

await transport.close();
logger.debug('Connection closed successfully');
try {
await runLegacyBasicClient(serverUrl);
} catch (error) {
legacyOrigins.delete(origin);
if (usedCachedLegacyDecision) {
await runVersionBackcompatClient(serverUrl);
return;
}
throw error;
}
}

registerScenarios(['initialize', 'tools_call', 'tools-call'], runBasicClient);
registerScenario('version-backcompat', runVersionBackcompatClient);

// SEP-2106: json-schema-ref-no-deref advertises a tool whose inputSchema
// contains a network-URI $ref. A conformant client lists tools normally and
Expand Down
104 changes: 104 additions & 0 deletions examples/servers/typescript/legacy-version-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import http from 'http';
import { DRAFT_PROTOCOL_VERSION } from '../../../src/types.js';
import { UNSUPPORTED_PROTOCOL_VERSION } from '../../../src/spec-types/draft.js';
import {
isDraftModernProbe,
LEGACY_PROTOCOL_VERSION,
parseJsonRecord,
readLimitedRequestBody
} from '../../../src/version-compat.js';

const port = Number.parseInt(process.env.PORT ?? '3010', 10);
const emitModernError = process.env.EMIT_MODERN_ERROR === 'true';

const server = http.createServer((request, response) => {
void handleRequest(request, response);
});
server.requestTimeout = 5_000;
server.headersTimeout = 5_000;

async function handleRequest(
request: http.IncomingMessage,
response: http.ServerResponse
): Promise<void> {
let rawBody: string;
try {
rawBody = await readLimitedRequestBody(request);
} catch (error) {
response.writeHead(error instanceof RangeError ? 413 : 400).end();
return;
}

const body = parseJsonRecord(rawBody);
if (!body) {
response.writeHead(400).end();
return;
}

if (isDraftModernProbe(request, body)) {
response.writeHead(400, { 'Content-Type': 'application/json' });
response.end(
JSON.stringify({
jsonrpc: '2.0',
id: body.id ?? null,
error: emitModernError
? {
code: UNSUPPORTED_PROTOCOL_VERSION,
message: 'Unsupported protocol version',
data: {
supported: [LEGACY_PROTOCOL_VERSION],
requested: DRAFT_PROTOCOL_VERSION
}
}
: { code: -32600, message: 'Invalid Request' }
})
);
return;
}

if (body.method === 'initialize') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(
JSON.stringify({
jsonrpc: '2.0',
id: body.id,
result: {
protocolVersion: LEGACY_PROTOCOL_VERSION,
capabilities: { tools: {} },
serverInfo: { name: 'legacy-version-server', version: '1.0.0' }
}
})
);
return;
}

if (body.method === 'notifications/initialized') {
response.writeHead(202).end();
return;
}

if (body.method === 'tools/list') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(
JSON.stringify({
jsonrpc: '2.0',
id: body.id,
result: { tools: [] }
})
);
return;
}

response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({ jsonrpc: '2.0', id: body.id, result: {} }));
}

server.listen(port, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Failed to determine legacy server port');
}
console.log(
`Legacy version server running on http://127.0.0.1:${address.port}/mcp`
);
});
Loading
Loading