Skip to content
Merged
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
45 changes: 45 additions & 0 deletions examples/servers/typescript/everything-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js
import {
ElicitResultSchema,
ListToolsRequestSchema,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
type ListToolsResult,
type Tool
} from '@modelcontextprotocol/sdk/types.js';
Expand Down Expand Up @@ -128,6 +131,46 @@ function createMcpServer() {
}
);

// SEP-2549: Wrap setRequestHandler so the SDK's own list handlers
// automatically get caching hints appended to their responses.
const originalSetRequestHandler = mcpServer.server.setRequestHandler.bind(
mcpServer.server
);
const listSchemasForCaching = new Set([
ListToolsRequestSchema,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema
]);
mcpServer.server.setRequestHandler = ((schema: any, handler: any) => {
if (listSchemasForCaching.has(schema)) {
return originalSetRequestHandler(schema, async (...args: any[]) => {
const result = await handler(...args);
return { ...result, ttlMs: 300000, cacheScope: 'public' as const };
});
}
return originalSetRequestHandler(schema, handler);
}) as typeof mcpServer.server.setRequestHandler;

const registerResourceWithCacheHints =
mcpServer.registerResource.bind(mcpServer);
mcpServer.registerResource = ((
name: string,
uriOrTemplate: string | ResourceTemplate,
config: any,
readCallback: any
) =>
registerResourceWithCacheHints(
name,
uriOrTemplate as any,
config,
async (...args: any[]) => ({
...(await readCallback(...args)),
ttlMs: 300000,
cacheScope: 'private' as const
})
)) as typeof mcpServer.registerResource;

// Helper to send log messages using the underlying server
function sendLog(
level:
Expand Down Expand Up @@ -1024,6 +1067,8 @@ function createMcpServer() {
_meta: tool._meta
};
})
// Note: SEP-2549 caching hints are added automatically by the
// setRequestHandler wrapper above
};
}
);
Expand Down
159 changes: 159 additions & 0 deletions examples/servers/typescript/sep-2549-no-caching-hints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env node

/**
* SEP-2549 Negative Test Server
*
* Returns list and read results WITHOUT ttlMs and cacheScope fields,
* violating the SEP-2549 MUST. The caching scenario should emit FAILURE
* for presence checks against this server.
*/

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import {
ListToolsRequestSchema,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ReadResourceRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import express from 'express';
import { randomUUID } from 'crypto';

const transports: Record<string, StreamableHTTPServerTransport> = {};

function isInitializeRequest(body: any): boolean {
return body?.method === 'initialize';
}

function createServer() {
const server = new Server(
{ name: 'sep-2549-no-caching-hints', version: '1.0.0' },
{
capabilities: {
tools: {},
resources: {},
prompts: {}
}
}
);

// Deliberately omit ttlMs and cacheScope from all responses
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'test_tool',
description: 'A test tool',
inputSchema: { type: 'object' as const }
}
]
}));

server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: [
{
name: 'test_prompt',
description: 'A test prompt'
}
]
}));

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'test://static-text',
name: 'Static Text',
description: 'A static text resource'
}
]
}));

server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
resourceTemplates: []
}));

server.setRequestHandler(ReadResourceRequestSchema, async () => ({
contents: [
{
uri: 'test://static-text',
mimeType: 'text/plain',
text: 'Static text content.'
}
]
}));

return server;
}

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

app.post('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;

try {
if (sessionId && transports[sessionId]) {
await transports[sessionId].handleRequest(req, res, req.body);
return;
}

if (!sessionId && isInitializeRequest(req.body)) {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (newSessionId) => {
transports[newSessionId] = transport;
}
});
transport.onclose = () => {
const sid = transport.sessionId;
if (sid) delete transports[sid];
};
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
return;
}

res.status(400).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Invalid or missing session ID' },
id: null
});
} catch (error) {
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: `Internal error: ${error instanceof Error ? error.message : String(error)}`
},
id: null
});
}
}
});

app.get('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (sessionId && transports[sessionId]) {
await transports[sessionId].handleRequest(req, res);
} else {
res.status(400).json({ error: 'Invalid or missing session ID' });
}
});

app.delete('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (sessionId && transports[sessionId]) {
await transports[sessionId].handleRequest(req, res);
} else {
res.status(400).json({ error: 'Invalid or missing session ID' });
}
});

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

import { DNSRebindingProtectionScenario } from './server/dns-rebinding';
import { CachingScenario } from './server/caching';

import {
HttpHeaderValidationScenario,
Expand Down Expand Up @@ -162,6 +163,8 @@ const allClientScenariosList: ClientScenario[] = [
// Security scenarios
new DNSRebindingProtectionScenario(),

// Caching scenarios (SEP-2549)
new CachingScenario(),
// HTTP Standardization scenarios (SEP-2243)
new HttpHeaderValidationScenario(),
new HttpCustomHeaderServerValidationScenario()
Expand Down
Loading
Loading