From def567b57401180764ef1519983b725a4baa3e1d Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Thu, 14 May 2026 16:17:36 -0700 Subject: [PATCH 1/2] initial conformance tests for SEP-2549 --- .../servers/typescript/everything-server.ts | 128 +++++++ .../typescript/sep-2549-no-caching-hints.ts | 159 ++++++++ src/scenarios/index.ts | 6 +- src/scenarios/server/caching.ts | 342 ++++++++++++++++++ src/scenarios/server/negative.test.ts | 42 +++ src/seps/sep-2549.yaml | 42 +++ 6 files changed, 718 insertions(+), 1 deletion(-) create mode 100644 examples/servers/typescript/sep-2549-no-caching-hints.ts create mode 100644 src/scenarios/server/caching.ts create mode 100644 src/seps/sep-2549.yaml diff --git a/examples/servers/typescript/everything-server.ts b/examples/servers/typescript/everything-server.ts index 91559f8a..06e6c016 100644 --- a/examples/servers/typescript/everything-server.ts +++ b/examples/servers/typescript/everything-server.ts @@ -22,6 +22,10 @@ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js import { ElicitResultSchema, ListToolsRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListResourceTemplatesRequestSchema, + ReadResourceRequestSchema, type ListToolsResult, type Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -1023,11 +1027,135 @@ function createMcpServer() { annotations: tool.annotations, _meta: tool._meta }; + }), + // SEP-2549: Caching hints + ttlMs: 300000, + cacheScope: 'public' as const + }; + } + ); + + // ===== SEP-2549: Override list/read handlers to include caching hints ===== + + mcpServer.server.setRequestHandler( + ListPromptsRequestSchema, + async () => { + const registeredPrompts = (mcpServer as any)._registeredPrompts as Record< + string, + { + enabled: boolean; + title?: string; + description?: string; + argsSchema?: any; + } + >; + + return { + prompts: Object.entries(registeredPrompts) + .filter(([, prompt]) => prompt.enabled) + .map(([name, prompt]) => ({ + name, + title: prompt.title, + description: prompt.description + })), + ttlMs: 300000, + cacheScope: 'public' as const + }; + } + ); + + mcpServer.server.setRequestHandler( + ListResourcesRequestSchema, + async () => { + const registeredResources = (mcpServer as any)._registeredResources as Record< + string, + { enabled: boolean; name: string; metadata?: any } + >; + + return { + resources: Object.entries(registeredResources) + .filter(([, res]) => res.enabled) + .map(([uri, res]) => ({ + uri, + name: res.name, + ...res.metadata + })), + ttlMs: 300000, + cacheScope: 'public' as const + }; + } + ); + + mcpServer.server.setRequestHandler( + ListResourceTemplatesRequestSchema, + async () => { + const registeredResourceTemplates = (mcpServer as any) + ._registeredResourceTemplates as Record< + string, + { resourceTemplate: any; metadata?: any } + >; + + return { + resourceTemplates: Object.entries(registeredResourceTemplates).map( + ([name, template]) => ({ + name, + uriTemplate: template.resourceTemplate.uriTemplate.toString(), + ...template.metadata }) + ), + ttlMs: 300000, + cacheScope: 'public' as const }; } ); + mcpServer.server.setRequestHandler( + ReadResourceRequestSchema, + async (request: any) => { + const uri = new URL(request.params.uri); + const registeredResources = (mcpServer as any)._registeredResources as Record< + string, + { enabled: boolean; readCallback: (uri: URL, extra?: any) => Promise } + >; + const registeredResourceTemplates = (mcpServer as any) + ._registeredResourceTemplates as Record< + string, + { + resourceTemplate: any; + readCallback: (uri: URL, variables: Record, extra?: any) => Promise; + } + >; + + // Exact resource match + const resource = registeredResources[uri.toString()]; + if (resource && resource.enabled) { + const result = await resource.readCallback(uri); + return { + ...result, + ttlMs: 300000, + cacheScope: 'private' as const + }; + } + + // Template match + for (const template of Object.values(registeredResourceTemplates)) { + const variables = template.resourceTemplate.uriTemplate.match( + uri.toString() + ); + if (variables) { + const result = await template.readCallback(uri, variables); + return { + ...result, + ttlMs: 300000, + cacheScope: 'private' as const + }; + } + } + + throw new Error(`Resource not found: ${uri}`); + } + ); + return mcpServer; } diff --git a/examples/servers/typescript/sep-2549-no-caching-hints.ts b/examples/servers/typescript/sep-2549-no-caching-hints.ts new file mode 100644 index 00000000..8fe7952d --- /dev/null +++ b/examples/servers/typescript/sep-2549-no-caching-hints.ts @@ -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 = {}; + +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` + ); +}); diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 396d3bce..732b6136 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -63,6 +63,7 @@ import { } from './server/prompts'; import { DNSRebindingProtectionScenario } from './server/dns-rebinding'; +import { CachingScenario } from './server/caching'; import { authScenariosList, @@ -140,7 +141,10 @@ const allClientScenariosList: ClientScenario[] = [ new PromptsGetWithImageScenario(), // Security scenarios - new DNSRebindingProtectionScenario() + new DNSRebindingProtectionScenario(), + + // Caching scenarios (SEP-2549) + new CachingScenario() ]; // Active client scenarios (excludes pending) diff --git a/src/scenarios/server/caching.ts b/src/scenarios/server/caching.ts new file mode 100644 index 00000000..b4c62194 --- /dev/null +++ b/src/scenarios/server/caching.ts @@ -0,0 +1,342 @@ +/** + * Caching (SEP-2549) test scenario for MCP servers + * + * Tests that servers include ttlMs and cacheScope on cacheable results: + * tools/list, prompts/list, resources/list, resources/templates/list, resources/read + */ + +import { + ClientScenario, + ConformanceCheck, + DRAFT_PROTOCOL_VERSION +} from '../../types'; +import { connectToServer } from './client-helper'; +import { + ListToolsResultSchema, + ListPromptsResultSchema, + ListResourcesResultSchema, + ListResourceTemplatesResultSchema, + ReadResourceResultSchema +} from '@modelcontextprotocol/sdk/types.js'; + +const SPEC_REFS = [ + { + id: 'MCP-Caching', + url: 'https://modelcontextprotocol.io/specification/draft/server/utilities/caching' + }, + { + id: 'SEP-2549', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549' + } +]; + +interface CachingFields { + ttlMs: unknown; + cacheScope: unknown; + hasTtlMs: boolean; + hasCacheScope: boolean; +} + +function extractCachingFields(result: Record): CachingFields { + const hasTtlMs = 'ttlMs' in result; + const hasCacheScope = 'cacheScope' in result; + return { + ttlMs: hasTtlMs ? result.ttlMs : undefined, + cacheScope: hasCacheScope ? result.cacheScope : undefined, + hasTtlMs, + hasCacheScope + }; +} + +function buildPresenceCheck( + id: string, + name: string, + endpoint: string, + fields: CachingFields +): ConformanceCheck { + const errors: string[] = []; + + if (!fields.hasTtlMs) { + errors.push(`${endpoint} response missing ttlMs`); + } + if (!fields.hasCacheScope) { + errors.push(`${endpoint} response missing cacheScope`); + } + + return { + id, + name, + description: `${endpoint} response includes ttlMs and cacheScope caching hints`, + status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: SPEC_REFS, + details: { + ttlMs: fields.ttlMs, + cacheScope: fields.cacheScope, + hasTtlMs: fields.hasTtlMs, + hasCacheScope: fields.hasCacheScope + } + }; +} + +export class CachingScenario implements ClientScenario { + name = 'caching'; + readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + description = `Test that servers include caching hints (ttlMs and cacheScope) on cacheable results (SEP-2549). + +**Server Implementation Requirements:** + +Servers MUST include \`ttlMs\` (integer >= 0) and \`cacheScope\` ("public" or "private") on results from: +- \`tools/list\` +- \`prompts/list\` +- \`resources/list\` +- \`resources/templates/list\` +- \`resources/read\``; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + const allFields: Array<{ endpoint: string; fields: CachingFields }> = []; + + try { + const connection = await connectToServer(serverUrl); + + // 1. tools/list + try { + const toolsResult = await connection.client.request( + { method: 'tools/list', params: {} }, + ListToolsResultSchema + ); + const fields = extractCachingFields( + toolsResult as Record + ); + allFields.push({ endpoint: 'tools/list', fields }); + checks.push( + buildPresenceCheck( + 'sep-2549-tools-list-caching-hints', + 'ToolsListCachingHints', + 'tools/list', + fields + ) + ); + } catch (error) { + checks.push({ + id: 'sep-2549-tools-list-caching-hints', + name: 'ToolsListCachingHints', + description: + 'tools/list response includes ttlMs and cacheScope caching hints', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `tools/list request failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: SPEC_REFS + }); + } + + // 2. prompts/list + try { + const promptsResult = await connection.client.request( + { method: 'prompts/list', params: {} }, + ListPromptsResultSchema + ); + const fields = extractCachingFields( + promptsResult as Record + ); + allFields.push({ endpoint: 'prompts/list', fields }); + checks.push( + buildPresenceCheck( + 'sep-2549-prompts-list-caching-hints', + 'PromptsListCachingHints', + 'prompts/list', + fields + ) + ); + } catch (error) { + checks.push({ + id: 'sep-2549-prompts-list-caching-hints', + name: 'PromptsListCachingHints', + description: + 'prompts/list response includes ttlMs and cacheScope caching hints', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `prompts/list request failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: SPEC_REFS + }); + } + + // 3. resources/list + try { + const resourcesResult = await connection.client.request( + { method: 'resources/list', params: {} }, + ListResourcesResultSchema + ); + const fields = extractCachingFields( + resourcesResult as Record + ); + allFields.push({ endpoint: 'resources/list', fields }); + checks.push( + buildPresenceCheck( + 'sep-2549-resources-list-caching-hints', + 'ResourcesListCachingHints', + 'resources/list', + fields + ) + ); + } catch (error) { + checks.push({ + id: 'sep-2549-resources-list-caching-hints', + name: 'ResourcesListCachingHints', + description: + 'resources/list response includes ttlMs and cacheScope caching hints', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `resources/list request failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: SPEC_REFS + }); + } + + // 4. resources/templates/list + try { + const templatesResult = await connection.client.request( + { method: 'resources/templates/list', params: {} }, + ListResourceTemplatesResultSchema + ); + const fields = extractCachingFields( + templatesResult as Record + ); + allFields.push({ endpoint: 'resources/templates/list', fields }); + checks.push( + buildPresenceCheck( + 'sep-2549-resources-templates-list-caching-hints', + 'ResourcesTemplatesListCachingHints', + 'resources/templates/list', + fields + ) + ); + } catch (error) { + checks.push({ + id: 'sep-2549-resources-templates-list-caching-hints', + name: 'ResourcesTemplatesListCachingHints', + description: + 'resources/templates/list response includes ttlMs and cacheScope caching hints', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `resources/templates/list request failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: SPEC_REFS + }); + } + + // 5. resources/read — use test://static-text from resources/list + try { + const readResult = await connection.client.request( + { + method: 'resources/read', + params: { uri: 'test://static-text' } + }, + ReadResourceResultSchema + ); + const fields = extractCachingFields( + readResult as Record + ); + allFields.push({ endpoint: 'resources/read', fields }); + checks.push( + buildPresenceCheck( + 'sep-2549-resources-read-caching-hints', + 'ResourcesReadCachingHints', + 'resources/read', + fields + ) + ); + } catch (error) { + checks.push({ + id: 'sep-2549-resources-read-caching-hints', + name: 'ResourcesReadCachingHints', + description: + 'resources/read response includes ttlMs and cacheScope caching hints', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `resources/read request failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: SPEC_REFS + }); + } + + // 6. Aggregate: ttlMs must be a non-negative integer + const ttlErrors: string[] = []; + for (const { endpoint, fields } of allFields) { + if (fields.hasTtlMs) { + const val = fields.ttlMs; + if (typeof val !== 'number') { + ttlErrors.push( + `${endpoint}: ttlMs is ${typeof val}, expected number` + ); + } else if (!Number.isInteger(val)) { + ttlErrors.push(`${endpoint}: ttlMs is ${val}, expected integer`); + } else if (val < 0) { + ttlErrors.push(`${endpoint}: ttlMs is ${val}, must be >= 0`); + } + } + } + + checks.push({ + id: 'sep-2549-ttl-non-negative', + name: 'TtlNonNegative', + description: 'All ttlMs values are non-negative integers', + status: ttlErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + ttlErrors.length > 0 ? ttlErrors.join('; ') : undefined, + specReferences: SPEC_REFS, + details: { + endpoints: allFields.map((f) => ({ + endpoint: f.endpoint, + ttlMs: f.fields.ttlMs + })) + } + }); + + // 7. Aggregate: cacheScope must be "public" or "private" + const scopeErrors: string[] = []; + for (const { endpoint, fields } of allFields) { + if (fields.hasCacheScope) { + const val = fields.cacheScope; + if (val !== 'public' && val !== 'private') { + scopeErrors.push( + `${endpoint}: cacheScope is ${JSON.stringify(val)}, expected "public" or "private"` + ); + } + } + } + + checks.push({ + id: 'sep-2549-cache-scope-valid', + name: 'CacheScopeValid', + description: 'All cacheScope values are "public" or "private"', + status: scopeErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + scopeErrors.length > 0 ? scopeErrors.join('; ') : undefined, + specReferences: SPEC_REFS, + details: { + endpoints: allFields.map((f) => ({ + endpoint: f.endpoint, + cacheScope: f.fields.cacheScope + })) + } + }); + + await connection.close(); + } catch (error) { + // Connection-level failure — push a single failure check + checks.push({ + id: 'sep-2549-tools-list-caching-hints', + name: 'ToolsListCachingHints', + description: 'Caching hints scenario failed to connect', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Connection failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: SPEC_REFS + }); + } + + return checks; + } +} diff --git a/src/scenarios/server/negative.test.ts b/src/scenarios/server/negative.test.ts index 769374a5..66b481c3 100644 --- a/src/scenarios/server/negative.test.ts +++ b/src/scenarios/server/negative.test.ts @@ -2,6 +2,7 @@ import { spawn, ChildProcess } from 'child_process'; import path from 'path'; import { DNSRebindingProtectionScenario } from './dns-rebinding'; import { ResourcesNotFoundErrorScenario } from './resources'; +import { CachingScenario } from './caching'; function startServer(scriptPath: string, port: number): Promise { return new Promise((resolve, reject) => { @@ -106,4 +107,45 @@ describe('Server scenario negative tests', () => { expect(errorCode?.status).toBe('WARNING'); }, 10000); }); + + describe('sep-2549-caching-hints', () => { + let serverProcess: ChildProcess | null = null; + const PORT = 3006; + + beforeAll(async () => { + serverProcess = await startServer( + path.join( + process.cwd(), + 'examples/servers/typescript/sep-2549-no-caching-hints.ts' + ), + PORT + ); + }, 35000); + + afterAll(async () => { + await stopServer(serverProcess); + }); + + it('emits FAILURE for presence checks against a server without caching hints', async () => { + const scenario = new CachingScenario(); + const checks = await scenario.run(`http://localhost:${PORT}/mcp`); + + // Should have at least 7 checks (5 presence + 2 aggregate) + expect(checks.length).toBeGreaterThanOrEqual(7); + + const presenceCheckIds = [ + 'sep-2549-tools-list-caching-hints', + 'sep-2549-prompts-list-caching-hints', + 'sep-2549-resources-list-caching-hints', + 'sep-2549-resources-templates-list-caching-hints', + 'sep-2549-resources-read-caching-hints' + ]; + + for (const checkId of presenceCheckIds) { + const check = checks.find((c) => c.id === checkId); + expect(check).toBeDefined(); + expect(check?.status).toBe('FAILURE'); + } + }, 15000); + }); }); diff --git a/src/seps/sep-2549.yaml b/src/seps/sep-2549.yaml new file mode 100644 index 00000000..be6c567a --- /dev/null +++ b/src/seps/sep-2549.yaml @@ -0,0 +1,42 @@ +sep: 2549 +spec_url: https://modelcontextprotocol.io/specification/draft/server/utilities/caching +requirements: + # Server MUST requirements (testable) + - check: sep-2549-tools-list-caching-hints + text: 'Servers MUST include caching hints on results returned by tools/list' + - check: sep-2549-prompts-list-caching-hints + text: 'Servers MUST include caching hints on results returned by prompts/list' + - check: sep-2549-resources-list-caching-hints + text: 'Servers MUST include caching hints on results returned by resources/list' + - check: sep-2549-resources-templates-list-caching-hints + text: 'Servers MUST include caching hints on results returned by resources/templates/list' + - check: sep-2549-resources-read-caching-hints + text: 'Servers MUST include caching hints on results returned by resources/read' + - check: sep-2549-ttl-non-negative + text: 'Servers MUST provide a ttlMs value that is >= 0' + - check: sep-2549-cache-scope-valid + text: 'cacheScope indicates the intended scope of the cached response, either "public" or "private"' + + # Client-side requirements (not protocol-observable from server tests) + - text: 'If ttlMs is 0, the response SHOULD be considered immediately stale' + excluded: 'Client-side caching behavior; not observable at the protocol level' + - text: 'If ttlMs is positive, the client SHOULD consider the result fresh for that many milliseconds' + excluded: 'Client-side caching behavior; not observable at the protocol level' + - text: 'If ttlMs is absent, clients SHOULD assume a default of 0' + excluded: 'Client-side caching behavior; not observable at the protocol level' + - text: 'If ttlMs is negative, clients SHOULD ignore it and treat it as 0' + excluded: 'Client-side caching behavior; not observable at the protocol level' + - text: 'Once the TTL expires, the response is stale and the client SHOULD re-fetch on next access' + excluded: 'Client-side caching behavior; not observable at the protocol level' + - text: 'Clients SHOULD NOT treat TTL as a polling interval that triggers automatic background refetches' + excluded: 'Client-side caching behavior; not observable at the protocol level' + - text: 'Implementations that do choose to poll MUST apply jitter and backoff' + excluded: 'Client-side polling behavior; not observable at the protocol level' + - text: 'Cached responses MAY be reused for the same authorization context. Caches MUST NOT be shared across authorization contexts' + excluded: 'Client/cache-side behavior; not observable at the protocol level' + + # Server security requirements (implementation guidance, not wire-observable) + - text: 'Servers MUST ensure that responses marked as "public" do not lead to primitives being exposed across users and auth context' + excluded: 'Server-side access control; implementation guidance not testable via protocol messages' + - text: 'Server authors MUST apply appropriate per-primitive access controls, and MUST NOT rely on cacheScope alone to prevent unauthorized access to primitives' + excluded: 'Server-side access control; implementation guidance not testable via protocol messages' From cd262c590999712d5166f096b67f4f43a99bcd01 Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Thu, 14 May 2026 16:19:38 -0700 Subject: [PATCH 2/2] apply linting --- .../servers/typescript/everything-server.ts | 105 +++++++++--------- .../authorization-server-metadata.ts | 4 +- src/scenarios/server/caching.ts | 3 +- 3 files changed, 58 insertions(+), 54 deletions(-) diff --git a/examples/servers/typescript/everything-server.ts b/examples/servers/typescript/everything-server.ts index 06e6c016..fc8072ed 100644 --- a/examples/servers/typescript/everything-server.ts +++ b/examples/servers/typescript/everything-server.ts @@ -1037,54 +1037,49 @@ function createMcpServer() { // ===== SEP-2549: Override list/read handlers to include caching hints ===== - mcpServer.server.setRequestHandler( - ListPromptsRequestSchema, - async () => { - const registeredPrompts = (mcpServer as any)._registeredPrompts as Record< - string, - { - enabled: boolean; - title?: string; - description?: string; - argsSchema?: any; - } - >; - - return { - prompts: Object.entries(registeredPrompts) - .filter(([, prompt]) => prompt.enabled) - .map(([name, prompt]) => ({ - name, - title: prompt.title, - description: prompt.description - })), - ttlMs: 300000, - cacheScope: 'public' as const - }; - } - ); - - mcpServer.server.setRequestHandler( - ListResourcesRequestSchema, - async () => { - const registeredResources = (mcpServer as any)._registeredResources as Record< - string, - { enabled: boolean; name: string; metadata?: any } - >; - - return { - resources: Object.entries(registeredResources) - .filter(([, res]) => res.enabled) - .map(([uri, res]) => ({ - uri, - name: res.name, - ...res.metadata - })), - ttlMs: 300000, - cacheScope: 'public' as const - }; - } - ); + mcpServer.server.setRequestHandler(ListPromptsRequestSchema, async () => { + const registeredPrompts = (mcpServer as any)._registeredPrompts as Record< + string, + { + enabled: boolean; + title?: string; + description?: string; + argsSchema?: any; + } + >; + + return { + prompts: Object.entries(registeredPrompts) + .filter(([, prompt]) => prompt.enabled) + .map(([name, prompt]) => ({ + name, + title: prompt.title, + description: prompt.description + })), + ttlMs: 300000, + cacheScope: 'public' as const + }; + }); + + mcpServer.server.setRequestHandler(ListResourcesRequestSchema, async () => { + const registeredResources = (mcpServer as any) + ._registeredResources as Record< + string, + { enabled: boolean; name: string; metadata?: any } + >; + + return { + resources: Object.entries(registeredResources) + .filter(([, res]) => res.enabled) + .map(([uri, res]) => ({ + uri, + name: res.name, + ...res.metadata + })), + ttlMs: 300000, + cacheScope: 'public' as const + }; + }); mcpServer.server.setRequestHandler( ListResourceTemplatesRequestSchema, @@ -1113,16 +1108,24 @@ function createMcpServer() { ReadResourceRequestSchema, async (request: any) => { const uri = new URL(request.params.uri); - const registeredResources = (mcpServer as any)._registeredResources as Record< + const registeredResources = (mcpServer as any) + ._registeredResources as Record< string, - { enabled: boolean; readCallback: (uri: URL, extra?: any) => Promise } + { + enabled: boolean; + readCallback: (uri: URL, extra?: any) => Promise; + } >; const registeredResourceTemplates = (mcpServer as any) ._registeredResourceTemplates as Record< string, { resourceTemplate: any; - readCallback: (uri: URL, variables: Record, extra?: any) => Promise; + readCallback: ( + uri: URL, + variables: Record, + extra?: any + ) => Promise; } >; diff --git a/src/scenarios/authorization-server/authorization-server-metadata.ts b/src/scenarios/authorization-server/authorization-server-metadata.ts index 00c930fc..50049603 100644 --- a/src/scenarios/authorization-server/authorization-server-metadata.ts +++ b/src/scenarios/authorization-server/authorization-server-metadata.ts @@ -9,7 +9,9 @@ import { request } from 'undici'; type Status = 'SUCCESS' | 'FAILURE'; -export class AuthorizationServerMetadataEndpointScenario implements ClientScenarioForAuthorizationServer { +export class AuthorizationServerMetadataEndpointScenario + implements ClientScenarioForAuthorizationServer +{ name = 'authorization-server-metadata-endpoint'; readonly source = { introducedIn: '2025-03-26' } as const; description = `Test authorization server metadata endpoint. diff --git a/src/scenarios/server/caching.ts b/src/scenarios/server/caching.ts index b4c62194..8068cc90 100644 --- a/src/scenarios/server/caching.ts +++ b/src/scenarios/server/caching.ts @@ -282,8 +282,7 @@ Servers MUST include \`ttlMs\` (integer >= 0) and \`cacheScope\` ("public" or "p description: 'All ttlMs values are non-negative integers', status: ttlErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), - errorMessage: - ttlErrors.length > 0 ? ttlErrors.join('; ') : undefined, + errorMessage: ttlErrors.length > 0 ? ttlErrors.join('; ') : undefined, specReferences: SPEC_REFS, details: { endpoints: allFields.map((f) => ({