/** * Cache hints (`CacheableResult`, protocol revision 2026-07-28). * * The 2026-07-28 revision requires `ttlMs`/`cacheScope` on the cacheable * result types (the list operations and `resources/read`). The values are * resolved most-specific-author-first: * * 1. fields the handler returns on the result itself, * 2. a per-registration `cacheHint` (here: the resource's read result), * 3. the server-level per-operation `ServerOptions.cacheHints`, * 4. the conservative defaults (`ttlMs: 0`, `cacheScope: 'private'`). * * The fields are emitted ONLY toward 2026-era clients — a 2025-era response * is byte-for-byte unchanged. One binary, either transport. */ import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; // Module-level (process-wide) counters so the values survive the stateless // HTTP leg (fresh `buildServer()` per request) as well as stdio's single // per-connection instance. The client asserts against these to prove a // cache-served call never reached the server. let readCount = 0; let listCount = 0; function buildServer(): McpServer { const server = new McpServer( { name: 'caching-example', version: '1.0.0' }, { // Server-level per-operation hints: any list/read result that does not // override a field gets these. cacheHints: { 'resources/list': { ttlMs: 5000, cacheScope: 'public' }, 'tools/list': { ttlMs: 30_000, cacheScope: 'public' } } } ); // A direct resource carrying a per-registration hint that wins for its // own resources/read result. server.registerResource( 'app-config', 'config://app', { mimeType: 'application/json', description: 'Static application config (rarely changes)', cacheHint: { ttlMs: 60_000, cacheScope: 'private' } }, async uri => { readCount++; return { contents: [{ uri: uri.href, mimeType: 'application/json', text: '{"feature":true}' }] }; } ); // A tool, so tools/list has something to cache. server.registerTool('noop', { description: 'no-op' }, async () => ({ content: [{ type: 'text', text: 'ok' }] })); // Exposes the server-side `resources/read` invocation count so the client // can assert that a cache-served call did not reach the wire. server.registerTool('read-count', { description: 'Number of resources/read calls that reached this server' }, async () => ({ content: [{ type: 'text', text: String(readCount) }] })); // Exposes the server-side `tools/list` invocation count. server.registerTool('request-count', { description: 'Number of tools/list requests that reached this server' }, async () => ({ content: [{ type: 'text', text: String(listCount) }] })); // Wrap the auto-generated `tools/list` handler so the example can prove a // cache-served `listTools()` never reached the wire. `McpServer` registers // the handler lazily on the first `registerTool()`; we re-seat it here so // every dispatch increments `listCount` before delegating to the original. // (Reaches the underlying request-handler map directly — there is no public // wrapper hook; acceptable for an instrumentation example.) const handlers = (server.server as unknown as { _requestHandlers: Map Promise> }) ._requestHandlers; const original = handlers.get('tools/list'); if (original) { handlers.set('tools/list', (...a) => { listCount++; return original(...a); }); } return server; } const { transport, port } = parseExampleArgs(); if (transport === 'stdio') { void serveStdio(buildServer); console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); // `createMcpHonoApp()` binds the endpoint behind localhost host/origin // validation by default, matching the framework factories' defaults. const app = createMcpHonoApp(); app.all('/mcp', c => handler.fetch(c.req.raw)); serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); }