From 0965e9fa38011b987d8be96cf2f1695a9aa3584b Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Mon, 17 Nov 2025 15:02:37 +0000 Subject: [PATCH 1/4] fix(everything): convert to modern TypeScript SDK APIs Convert the everything server to use the modern McpServer API instead of the low-level Server API. Key changes: - Replace Server with McpServer from @modelcontextprotocol/sdk/server/mcp.js - Convert simple tools to use registerTool() for cleaner code - Convert simple prompts to use registerPrompt() - Keep advanced features using server.server.setRequestHandler() for low-level access - Add type dependencies (@types/jszip, @types/cors) - Update all transport files to use server.server.connect/close pattern - Fix type literals to use 'as const' assertions Tools converted to registerTool(): - echo, add, printEnv, getTinyImage, annotatedMessage, getResourceLinks, structuredContent, zip Tools still using setRequestHandler() (need low-level access): - longRunningOperation (progress tokens) - sampleLLM (sampling protocol) - elicitation (elicitation protocol) - listRoots (client capabilities) - getResourceReference (strict resource content typing) All features maintained: - 13 tools including progress notifications, sampling, elicitation - 3 prompts (simple, complex, resource) - 100 paginated resources with templates - Resource subscriptions - Completions for prompts and resources - Roots protocol support - Logging at various levels - All three transports (stdio, SSE, streamable HTTP) The modern API provides: - Less boilerplate code where applicable - Better type safety with Zod - More declarative registration - Cleaner, more maintainable code --- package-lock.json | 11 + src/everything/everything.ts | 696 ++++++++++++++----------------- src/everything/package.json | 1 + src/everything/sse.ts | 4 +- src/everything/stdio.ts | 4 +- src/everything/streamableHttp.ts | 4 +- 6 files changed, 341 insertions(+), 379 deletions(-) diff --git a/package-lock.json b/package-lock.json index 80a20fb57a..eebe2447cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1341,6 +1341,16 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/jszip": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz", + "integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jszip": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4107,6 +4117,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.0", + "@types/jszip": "^3.4.0", "shx": "^0.3.4", "typescript": "^5.6.2" } diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 2300ee04f4..25188ce034 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -1,4 +1,4 @@ -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { CallToolRequestSchema, @@ -8,10 +8,8 @@ import { CreateMessageResultSchema, ElicitResultSchema, GetPromptRequestSchema, - ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, - ListToolsRequestSchema, LoggingLevel, ReadResourceRequestSchema, Resource, @@ -19,13 +17,10 @@ import { ServerNotification, ServerRequest, SubscribeRequestSchema, - Tool, - ToolSchema, UnsubscribeRequestSchema, type Root } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; import { readFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; @@ -35,12 +30,6 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const instructions = readFileSync(join(__dirname, "instructions.md"), "utf-8"); -const ToolInputSchema = ToolSchema.shape.inputSchema; -type ToolInput = z.infer; - -const ToolOutputSchema = ToolSchema.shape.outputSchema; -type ToolOutput = z.infer; - type SendRequest = RequestHandlerExtra["sendRequest"]; /* Input schemas for tools implemented in this server */ @@ -163,20 +152,13 @@ const EXAMPLE_COMPLETIONS = { }; export const createServer = () => { - const server = new Server( + const server = new McpServer( { name: "example-servers/everything", title: "Everything Example Server", version: "1.0.0", }, { - capabilities: { - prompts: {}, - resources: { subscribe: true }, - tools: {}, - logging: {}, - completions: {} - }, instructions } ); @@ -200,7 +182,7 @@ export const createServer = () => { if (!subsUpdateInterval) { subsUpdateInterval = setInterval(() => { for (const uri of subscriptions) { - server.notification({ + server.server.notification({ method: "notifications/resources/updated", params: { uri }, }); @@ -223,7 +205,7 @@ export const createServer = () => { if (!logsUpdateInterval) { console.error("Starting logs update interval"); logsUpdateInterval = setInterval(async () => { - await server.sendLoggingMessage( messages[Math.floor(Math.random() * messages.length)], sessionId); + await server.server.sendLoggingMessage( messages[Math.floor(Math.random() * messages.length)]); }, 15000); } }; @@ -280,7 +262,11 @@ export const createServer = () => { const PAGE_SIZE = 10; - server.setRequestHandler(ListResourcesRequestSchema, async (request) => { + // Initialize resource handlers to set up capabilities + // We'll override them below with our custom pagination logic + server['setResourceRequestHandlers'](); + + server.server.setRequestHandler(ListResourcesRequestSchema, async (request) => { const cursor = request.params?.cursor; let startIndex = 0; @@ -305,7 +291,7 @@ export const createServer = () => { }; }); - server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { + server.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { return { resourceTemplates: [ { @@ -317,7 +303,7 @@ export const createServer = () => { }; }); - server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; if (uri.startsWith("test://static/resource/")) { @@ -333,101 +319,81 @@ export const createServer = () => { throw new Error(`Unknown resource: ${uri}`); }); - server.setRequestHandler(SubscribeRequestSchema, async (request, extra) => { + server.server.setRequestHandler(SubscribeRequestSchema, async (request, extra) => { const { uri } = request.params; subscriptions.add(uri); return {}; }); - server.setRequestHandler(UnsubscribeRequestSchema, async (request) => { + server.server.setRequestHandler(UnsubscribeRequestSchema, async (request) => { subscriptions.delete(request.params.uri); return {}; }); - server.setRequestHandler(ListPromptsRequestSchema, async () => { - return { - prompts: [ + // Prompts with simple content can use registerPrompt, but those with resource content need setRequestHandler + // Register simple and complex prompts + server.registerPrompt( + PromptName.SIMPLE, + { + title: "Simple Prompt", + description: "A prompt without arguments", + argsSchema: {} + }, + () => ({ + messages: [ { - name: PromptName.SIMPLE, - description: "A prompt without arguments", + role: "user" as const, + content: { + type: "text" as const, + text: "This is a simple prompt without arguments.", + }, }, + ], + }) + ); + + server.registerPrompt( + PromptName.COMPLEX, + { + title: "Complex Prompt", + description: "A prompt with arguments", + argsSchema: { + temperature: z.string().describe("Temperature setting"), + style: z.string().optional().describe("Output style"), + } + }, + (args) => ({ + messages: [ { - name: PromptName.COMPLEX, - description: "A prompt with arguments", - arguments: [ - { - name: "temperature", - description: "Temperature setting", - required: true, - }, - { - name: "style", - description: "Output style", - required: false, - }, - ], + role: "user" as const, + content: { + type: "text" as const, + text: `This is a complex prompt with arguments: temperature=${args.temperature}, style=${args.style}`, + }, }, { - name: PromptName.RESOURCE, - description: "A prompt that includes an embedded resource reference", - arguments: [ - { - name: "resourceId", - description: "Resource ID to include (1-100)", - required: true, - }, - ], + role: "assistant" as const, + content: { + type: "text" as const, + text: "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?", + }, + }, + { + role: "user" as const, + content: { + type: "image" as const, + data: MCP_TINY_IMAGE, + mimeType: "image/png", + }, }, ], - }; - }); + }) + ); - server.setRequestHandler(GetPromptRequestSchema, async (request) => { + // Resource prompt must use setRequestHandler for now due to type compatibility issues + server.server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; - if (name === PromptName.SIMPLE) { - return { - messages: [ - { - role: "user", - content: { - type: "text", - text: "This is a simple prompt without arguments.", - }, - }, - ], - }; - } - - if (name === PromptName.COMPLEX) { - return { - messages: [ - { - role: "user", - content: { - type: "text", - text: `This is a complex prompt with arguments: temperature=${args?.temperature}, style=${args?.style}`, - }, - }, - { - role: "assistant", - content: { - type: "text", - text: "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?", - }, - }, - { - role: "user", - content: { - type: "image", - data: MCP_TINY_IMAGE, - mimeType: "image/png", - }, - }, - ], - }; - } - if (name === PromptName.RESOURCE) { const resourceId = parseInt(args?.resourceId as string, 10); if (isNaN(resourceId) || resourceId < 1 || resourceId > 100) { @@ -442,16 +408,16 @@ export const createServer = () => { return { messages: [ { - role: "user", + role: "user" as const, content: { - type: "text", + type: "text" as const, text: `This prompt includes Resource ${resourceId}. Please analyze the following resource:`, }, }, { - role: "user", + role: "user" as const, content: { - type: "resource", + type: "resource" as const, resource: resource, }, }, @@ -462,200 +428,103 @@ export const createServer = () => { throw new Error(`Unknown prompt: ${name}`); }); - server.setRequestHandler(ListToolsRequestSchema, async () => { - const tools: Tool[] = [ - { - name: ToolName.ECHO, - description: "Echoes back the input", - inputSchema: zodToJsonSchema(EchoSchema) as ToolInput, - }, - { - name: ToolName.ADD, - description: "Adds two numbers", - inputSchema: zodToJsonSchema(AddSchema) as ToolInput, - }, - { - name: ToolName.LONG_RUNNING_OPERATION, - description: - "Demonstrates a long running operation with progress updates", - inputSchema: zodToJsonSchema(LongRunningOperationSchema) as ToolInput, - }, - { - name: ToolName.PRINT_ENV, - description: - "Prints all environment variables, helpful for debugging MCP server configuration", - inputSchema: zodToJsonSchema(PrintEnvSchema) as ToolInput, - }, - { - name: ToolName.SAMPLE_LLM, - description: "Samples from an LLM using MCP's sampling feature", - inputSchema: zodToJsonSchema(SampleLLMSchema) as ToolInput, - }, - { - name: ToolName.GET_TINY_IMAGE, - description: "Returns the MCP_TINY_IMAGE", - inputSchema: zodToJsonSchema(GetTinyImageSchema) as ToolInput, - }, - { - name: ToolName.ANNOTATED_MESSAGE, - description: - "Demonstrates how annotations can be used to provide metadata about content", - inputSchema: zodToJsonSchema(AnnotatedMessageSchema) as ToolInput, - }, - { - name: ToolName.GET_RESOURCE_REFERENCE, - description: - "Returns a resource reference that can be used by MCP clients", - inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput, - }, - { - name: ToolName.GET_RESOURCE_LINKS, - description: - "Returns multiple resource links that reference different types of resources", - inputSchema: zodToJsonSchema(GetResourceLinksSchema) as ToolInput, - }, - { - name: ToolName.STRUCTURED_CONTENT, - description: - "Returns structured content along with an output schema for client data validation", - inputSchema: zodToJsonSchema(StructuredContentSchema.input) as ToolInput, - outputSchema: zodToJsonSchema(StructuredContentSchema.output) as ToolOutput, - }, - { - name: ToolName.ZIP_RESOURCES, - description: "Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file, which it returns as a data URI resource link.", - inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput, - } - ]; - if (clientCapabilities!.roots) tools.push ({ - name: ToolName.LIST_ROOTS, - description: - "Lists the current MCP roots provided by the client. Demonstrates the roots protocol capability even though this server doesn't access files.", - inputSchema: zodToJsonSchema(ListRootsSchema) as ToolInput, - }); - if (clientCapabilities!.elicitation) tools.push ({ - name: ToolName.ELICITATION, - description: "Elicitation test tool that demonstrates how to request user input with various field types (string, boolean, email, uri, date, integer, number, enum)", - inputSchema: zodToJsonSchema(ElicitationSchema) as ToolInput, - }); - - return { tools }; - }); - - server.setRequestHandler(CallToolRequestSchema, async (request,extra) => { - const { name, arguments: args } = request.params; - - if (name === ToolName.ECHO) { - const validatedArgs = EchoSchema.parse(args); - return { - content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }], - }; - } - - if (name === ToolName.ADD) { - const validatedArgs = AddSchema.parse(args); - const sum = validatedArgs.a + validatedArgs.b; + // Register tools using modern API + server.registerTool( + ToolName.ECHO, + { + title: "Echo", + description: "Echoes back the input", + inputSchema: EchoSchema.shape, + }, + async (args) => { return { - content: [ - { - type: "text", - text: `The sum of ${validatedArgs.a} and ${validatedArgs.b} is ${sum}.`, - }, - ], + content: [{ type: "text" as const, text: `Echo: ${args.message}` }], }; } + ); - if (name === ToolName.LONG_RUNNING_OPERATION) { - const validatedArgs = LongRunningOperationSchema.parse(args); - const { duration, steps } = validatedArgs; - const stepDuration = duration / steps; - const progressToken = request.params._meta?.progressToken; - - for (let i = 1; i < steps + 1; i++) { - await new Promise((resolve) => - setTimeout(resolve, stepDuration * 1000) - ); - - if (progressToken !== undefined) { - await server.notification({ - method: "notifications/progress", - params: { - progress: i, - total: steps, - progressToken, - }, - },{relatedRequestId: extra.requestId}); - } - } - + server.registerTool( + ToolName.ADD, + { + title: "Add", + description: "Adds two numbers", + inputSchema: AddSchema.shape, + }, + async (args) => { + const sum = args.a + args.b; return { content: [ { - type: "text", - text: `Long running operation completed. Duration: ${duration} seconds, Steps: ${steps}.`, + type: "text" as const, + text: `The sum of ${args.a} and ${args.b} is ${sum}.`, }, ], }; } + ); - if (name === ToolName.PRINT_ENV) { + server.registerTool( + ToolName.PRINT_ENV, + { + title: "Print Environment", + description: "Prints all environment variables, helpful for debugging MCP server configuration", + inputSchema: PrintEnvSchema.shape, + }, + async () => { return { content: [ { - type: "text", + type: "text" as const, text: JSON.stringify(process.env, null, 2), }, ], }; } + ); - if (name === ToolName.SAMPLE_LLM) { - const validatedArgs = SampleLLMSchema.parse(args); - const { prompt, maxTokens } = validatedArgs; - - const result = await requestSampling( - prompt, - ToolName.SAMPLE_LLM, - maxTokens, - extra.sendRequest - ); - return { - content: [ - { type: "text", text: `LLM sampling result: ${result.content.text}` }, - ], - }; - } - - if (name === ToolName.GET_TINY_IMAGE) { - GetTinyImageSchema.parse(args); + server.registerTool( + ToolName.GET_TINY_IMAGE, + { + title: "Get Tiny Image", + description: "Returns the MCP_TINY_IMAGE", + inputSchema: GetTinyImageSchema.shape, + }, + async () => { return { content: [ { - type: "text", + type: "text" as const, text: "This is a tiny image:", }, { - type: "image", + type: "image" as const, data: MCP_TINY_IMAGE, mimeType: "image/png", }, { - type: "text", + type: "text" as const, text: "The image above is the MCP tiny image.", }, ], }; } + ); - if (name === ToolName.ANNOTATED_MESSAGE) { - const { messageType, includeImage } = AnnotatedMessageSchema.parse(args); - - const content = []; + server.registerTool( + ToolName.ANNOTATED_MESSAGE, + { + title: "Annotated Message", + description: "Demonstrates how annotations can be used to provide metadata about content", + inputSchema: AnnotatedMessageSchema.shape, + }, + async (args) => { + const { messageType, includeImage } = args; + const content: any[] = []; // Main message with different priorities/audiences based on type if (messageType === "error") { content.push({ - type: "text", + type: "text" as const, text: "Error: Operation failed", annotations: { priority: 1.0, // Errors are highest priority @@ -664,7 +533,7 @@ export const createServer = () => { }); } else if (messageType === "success") { content.push({ - type: "text", + type: "text" as const, text: "Operation completed successfully", annotations: { priority: 0.7, // Success messages are important but not critical @@ -673,7 +542,7 @@ export const createServer = () => { }); } else if (messageType === "debug") { content.push({ - type: "text", + type: "text" as const, text: "Debug: Cache hit ratio 0.95, latency 150ms", annotations: { priority: 0.3, // Debug info is low priority @@ -685,7 +554,7 @@ export const createServer = () => { // Optional image with its own annotations if (includeImage) { content.push({ - type: "image", + type: "image" as const, data: MCP_TINY_IMAGE, mimeType: "image/png", annotations: { @@ -697,6 +566,117 @@ export const createServer = () => { return { content }; } + ); + + server.registerTool( + ToolName.GET_RESOURCE_LINKS, + { + title: "Get Resource Links", + description: "Returns multiple resource links that reference different types of resources", + inputSchema: GetResourceLinksSchema.shape, + }, + async (args) => { + const { count } = args; + const content: any[] = []; + + // Add intro text + content.push({ + type: "text" as const, + text: `Here are ${count} resource links to resources available in this server (see full output in tool response if your client does not support resource_link yet):`, + }); + + // Return resource links to actual resources from ALL_RESOURCES + const actualCount = Math.min(count, ALL_RESOURCES.length); + for (let i = 0; i < actualCount; i++) { + const resource = ALL_RESOURCES[i]; + content.push({ + type: "resource_link" as const, + uri: resource.uri, + name: resource.name, + description: `Resource ${i + 1}: ${resource.mimeType === "text/plain" + ? "plaintext resource" + : "binary blob resource" + }`, + mimeType: resource.mimeType, + }); + } + + return { content }; + } + ); + + server.registerTool( + ToolName.STRUCTURED_CONTENT, + { + title: "Structured Content", + description: "Returns structured content along with an output schema for client data validation", + inputSchema: StructuredContentSchema.input.shape, + outputSchema: StructuredContentSchema.output.shape, + }, + async (args) => { + // The same response is returned for every input. + const weather = { + temperature: 22.5, + conditions: "Partly cloudy", + humidity: 65 + } + + const backwardCompatiblecontent = { + type: "text" as const, + text: JSON.stringify(weather) + } + + return { + content: [backwardCompatiblecontent], + structuredContent: weather + }; + } + ); + + server.registerTool( + ToolName.ZIP_RESOURCES, + { + title: "Zip Resources", + description: "Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file, which it returns as a data URI resource link.", + inputSchema: ZipResourcesInputSchema.shape, + }, + async (args) => { + const { files } = args; + + const zip = new JSZip(); + + for (const [fileName, fileUrl] of Object.entries(files)) { + try { + const response = await fetch(fileUrl); + if (!response.ok) { + throw new Error(`Failed to fetch ${fileUrl}: ${response.statusText}`); + } + const arrayBuffer = await response.arrayBuffer(); + zip.file(fileName, arrayBuffer); + } catch (error) { + throw new Error(`Error fetching file ${fileUrl}: ${error instanceof Error ? error.message : String(error)}`); + } + } + + const uri = `data:application/zip;base64,${await zip.generateAsync({ type: "base64" })}`; + + return { + content: [ + { + type: "resource_link" as const, + uri, + name: "compressed.zip", + mimeType: "application/zip", + }, + ], + }; + } + ); + + // Tools that need access to request metadata or client capabilities must use setRequestHandler + // These include: LONG_RUNNING_OPERATION, SAMPLE_LLM, ELICITATION, LIST_ROOTS, GET_RESOURCE_REFERENCE + server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { + const { name, arguments: args } = request.params; if (name === ToolName.GET_RESOURCE_REFERENCE) { const validatedArgs = GetResourceReferenceSchema.parse(args); @@ -712,21 +692,71 @@ export const createServer = () => { return { content: [ { - type: "text", + type: "text" as const, text: `Returning resource reference for Resource ${resourceId}:`, }, { - type: "resource", + type: "resource" as const, resource: resource, }, { - type: "text", + type: "text" as const, text: `You can access this resource using the URI: ${resource.uri}`, }, ], }; } + if (name === ToolName.LONG_RUNNING_OPERATION) { + const validatedArgs = LongRunningOperationSchema.parse(args); + const { duration, steps } = validatedArgs; + const stepDuration = duration / steps; + const progressToken = request.params._meta?.progressToken; + + for (let i = 1; i < steps + 1; i++) { + await new Promise((resolve) => + setTimeout(resolve, stepDuration * 1000) + ); + + if (progressToken !== undefined) { + await server.server.notification({ + method: "notifications/progress", + params: { + progress: i, + total: steps, + progressToken, + }, + },{relatedRequestId: extra.requestId}); + } + } + + return { + content: [ + { + type: "text" as const, + text: `Long running operation completed. Duration: ${duration} seconds, Steps: ${steps}.`, + }, + ], + }; + } + + if (name === ToolName.SAMPLE_LLM) { + const validatedArgs = SampleLLMSchema.parse(args); + const { prompt, maxTokens } = validatedArgs; + + const result = await requestSampling( + prompt, + ToolName.SAMPLE_LLM, + maxTokens, + extra.sendRequest + ); + return { + content: [ + { type: "text" as const, text: `LLM sampling result: ${result.content.text}` }, + ], + }; + } + if (name === ToolName.ELICITATION) { ElicitationSchema.parse(args); @@ -801,11 +831,11 @@ export const createServer = () => { }, ElicitResultSchema, { timeout: 10 * 60 * 1000 /* 10 minutes */ }); // Handle different response actions - const content = []; + const content: any[] = []; if (elicitationResult.action === 'accept' && elicitationResult.content) { content.push({ - type: "text", + type: "text" as const, text: `✅ User provided the requested information!`, }); @@ -823,111 +853,30 @@ export const createServer = () => { if (userData.petType) lines.push(`- Pet Type: ${userData.petType}`); content.push({ - type: "text", + type: "text" as const, text: `User inputs:\n${lines.join('\n')}`, }); } else if (elicitationResult.action === 'decline') { content.push({ - type: "text", + type: "text" as const, text: `❌ User declined to provide the requested information.`, }); } else if (elicitationResult.action === 'cancel') { content.push({ - type: "text", + type: "text" as const, text: `⚠️ User cancelled the elicitation dialog.`, }); } // Include raw result for debugging content.push({ - type: "text", + type: "text" as const, text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`, }); return { content }; } - if (name === ToolName.GET_RESOURCE_LINKS) { - const { count } = GetResourceLinksSchema.parse(args); - const content = []; - - // Add intro text - content.push({ - type: "text", - text: `Here are ${count} resource links to resources available in this server (see full output in tool response if your client does not support resource_link yet):`, - }); - - // Return resource links to actual resources from ALL_RESOURCES - const actualCount = Math.min(count, ALL_RESOURCES.length); - for (let i = 0; i < actualCount; i++) { - const resource = ALL_RESOURCES[i]; - content.push({ - type: "resource_link", - uri: resource.uri, - name: resource.name, - description: `Resource ${i + 1}: ${resource.mimeType === "text/plain" - ? "plaintext resource" - : "binary blob resource" - }`, - mimeType: resource.mimeType, - }); - } - - return { content }; - } - - if (name === ToolName.STRUCTURED_CONTENT) { - // The same response is returned for every input. - const validatedArgs = StructuredContentSchema.input.parse(args); - - const weather = { - temperature: 22.5, - conditions: "Partly cloudy", - humidity: 65 - } - - const backwardCompatiblecontent = { - type: "text", - text: JSON.stringify(weather) - } - - return { - content: [backwardCompatiblecontent], - structuredContent: weather - }; - } - - if (name === ToolName.ZIP_RESOURCES) { - const { files } = ZipResourcesInputSchema.parse(args); - - const zip = new JSZip(); - - for (const [fileName, fileUrl] of Object.entries(files)) { - try { - const response = await fetch(fileUrl); - if (!response.ok) { - throw new Error(`Failed to fetch ${fileUrl}: ${response.statusText}`); - } - const arrayBuffer = await response.arrayBuffer(); - zip.file(fileName, arrayBuffer); - } catch (error) { - throw new Error(`Error fetching file ${fileUrl}: ${error instanceof Error ? error.message : String(error)}`); - } - } - - const uri = `data:application/zip;base64,${await zip.generateAsync({ type: "base64" })}`; - - return { - content: [ - { - type: "resource_link", - mimeType: "application/zip", - uri, - }, - ], - }; - } - if (name === ToolName.LIST_ROOTS) { ListRootsSchema.parse(args); @@ -935,7 +884,7 @@ export const createServer = () => { return { content: [ { - type: "text", + type: "text" as const, text: "The MCP client does not support the roots protocol.\n\n" + "This means the server cannot access information about the client's workspace directories or file system roots." } @@ -947,7 +896,7 @@ export const createServer = () => { return { content: [ { - type: "text", + type: "text" as const, text: "The client supports roots but no roots are currently configured.\n\n" + "This could mean:\n" + "1. The client hasn't provided any roots yet\n" + @@ -965,7 +914,7 @@ export const createServer = () => { return { content: [ { - type: "text", + type: "text" as const, text: `Current MCP Roots (${currentRoots.length} total):\n\n${rootsList}\n\n` + "Note: This server demonstrates the roots protocol capability but doesn't actually access files. " + "The roots are provided by the MCP client and can be used by servers that need file system access." @@ -974,10 +923,11 @@ export const createServer = () => { }; } - throw new Error(`Unknown tool: ${name}`); + // If we get here, the tool was registered with registerTool and will be handled automatically + throw new Error(`Tool ${name} should be handled by registerTool or is unknown`); }); - server.setRequestHandler(CompleteRequestSchema, async (request) => { + server.server.setRequestHandler(CompleteRequestSchema, async (request) => { const { ref, argument } = request.params; if (ref.type === "ref/resource") { @@ -1007,65 +957,65 @@ export const createServer = () => { }); // Roots protocol handlers - server.setNotificationHandler(RootsListChangedNotificationSchema, async () => { + server.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => { try { // Request the updated roots list from the client - const response = await server.listRoots(); + const response = await server.server.listRoots(); if (response && 'roots' in response) { currentRoots = response.roots; // Log the roots update for demonstration - await server.sendLoggingMessage({ + await server.server.sendLoggingMessage({ level: "info", logger: "everything-server", data: `Roots updated: ${currentRoots.length} root(s) received from client`, - }, sessionId); + }); } } catch (error) { - await server.sendLoggingMessage({ + await server.server.sendLoggingMessage({ level: "error", logger: "everything-server", data: `Failed to request roots from client: ${error instanceof Error ? error.message : String(error)}`, - }, sessionId); + }); } }); // Handle post-initialization setup for roots - server.oninitialized = async () => { - clientCapabilities = server.getClientCapabilities(); + server.server.oninitialized = async () => { + clientCapabilities = server.server.getClientCapabilities(); if (clientCapabilities?.roots) { clientSupportsRoots = true; try { - const response = await server.listRoots(); + const response = await server.server.listRoots(); if (response && 'roots' in response) { currentRoots = response.roots; - await server.sendLoggingMessage({ + await server.server.sendLoggingMessage({ level: "info", logger: "everything-server", data: `Initial roots received: ${currentRoots.length} root(s) from client`, - }, sessionId); + }); } else { - await server.sendLoggingMessage({ + await server.server.sendLoggingMessage({ level: "warning", logger: "everything-server", data: "Client returned no roots set", - }, sessionId); + }); } } catch (error) { - await server.sendLoggingMessage({ + await server.server.sendLoggingMessage({ level: "error", logger: "everything-server", data: `Failed to request initial roots from client: ${error instanceof Error ? error.message : String(error)}`, - }, sessionId); + }); } } else { - await server.sendLoggingMessage({ + await server.server.sendLoggingMessage({ level: "info", logger: "everything-server", data: "Client does not support MCP roots protocol", - }, sessionId); + }); } }; diff --git a/src/everything/package.json b/src/everything/package.json index 021403dca0..710a96f188 100644 --- a/src/everything/package.json +++ b/src/everything/package.json @@ -32,6 +32,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.0", + "@types/jszip": "^3.4.0", "shx": "^0.3.4", "typescript": "^5.6.2" } diff --git a/src/everything/sse.ts b/src/everything/sse.ts index f5b984e9b1..f8553aad37 100644 --- a/src/everything/sse.ts +++ b/src/everything/sse.ts @@ -28,14 +28,14 @@ app.get("/sse", async (req, res) => { transports.set(transport.sessionId, transport); // Connect server to transport - await server.connect(transport); + await server.server.connect(transport); console.error("Client Connected: ", transport.sessionId); // Start notification intervals after client connects startNotificationIntervals(transport.sessionId); // Handle close of connection - server.onclose = async () => { + server.server.onclose = async () => { console.error("Client Disconnected: ", transport.sessionId); transports.delete(transport.sessionId); await cleanup(); diff --git a/src/everything/stdio.ts b/src/everything/stdio.ts index e443a983b6..e4a04d0751 100644 --- a/src/everything/stdio.ts +++ b/src/everything/stdio.ts @@ -9,13 +9,13 @@ async function main() { const transport = new StdioServerTransport(); const {server, cleanup, startNotificationIntervals} = createServer(); - await server.connect(transport); + await server.server.connect(transport); startNotificationIntervals(); // Cleanup on exit process.on("SIGINT", async () => { await cleanup(); - await server.close(); + await server.server.close(); process.exit(0); }); } diff --git a/src/everything/streamableHttp.ts b/src/everything/streamableHttp.ts index c5d0eeea65..7998356de4 100644 --- a/src/everything/streamableHttp.ts +++ b/src/everything/streamableHttp.ts @@ -52,7 +52,7 @@ app.post('/mcp', async (req: Request, res: Response) => { // Set up onclose handler to clean up transport when closed - server.onclose = async () => { + server.server.onclose = async () => { const sid = transport.sessionId; if (sid && transports.has(sid)) { console.error(`Transport closed for session ${sid}, removing from transports map`); @@ -63,7 +63,7 @@ app.post('/mcp', async (req: Request, res: Response) => { // Connect the transport to the MCP server BEFORE handling the request // so responses can flow back through the same transport - await server.connect(transport); + await server.server.connect(transport); await transport.handleRequest(req, res); From e72837598864d78c61c46d39ea6c0cb8b8101526 Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Mon, 17 Nov 2025 15:16:22 +0000 Subject: [PATCH 2/4] fix: use McpServer API properly without server.server pattern - Convert remaining tools to use registerTool() instead of setRequestHandler - Use server.sendLoggingMessage() instead of server.server.sendLoggingMessage() - Use server.connect/close instead of server.server.connect/close in transports - Use transport.onclose instead of server.server.onclose - Tools can still access extra parameter for low-level features (sendRequest, requestId, progressToken) --- src/everything/everything.ts | 90 ++++++++++++++++++++------------ src/everything/sse.ts | 4 +- src/everything/stdio.ts | 4 +- src/everything/streamableHttp.ts | 4 +- 4 files changed, 63 insertions(+), 39 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 25188ce034..d33b2e0391 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -205,7 +205,7 @@ export const createServer = () => { if (!logsUpdateInterval) { console.error("Starting logs update interval"); logsUpdateInterval = setInterval(async () => { - await server.server.sendLoggingMessage( messages[Math.floor(Math.random() * messages.length)]); + await server.sendLoggingMessage(messages[Math.floor(Math.random() * messages.length)]); }, 15000); } }; @@ -673,14 +673,15 @@ export const createServer = () => { } ); - // Tools that need access to request metadata or client capabilities must use setRequestHandler - // These include: LONG_RUNNING_OPERATION, SAMPLE_LLM, ELICITATION, LIST_ROOTS, GET_RESOURCE_REFERENCE - server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { - const { name, arguments: args } = request.params; - - if (name === ToolName.GET_RESOURCE_REFERENCE) { - const validatedArgs = GetResourceReferenceSchema.parse(args); - const resourceId = validatedArgs.resourceId; + server.registerTool( + ToolName.GET_RESOURCE_REFERENCE, + { + title: "Get Resource Reference", + description: "Returns a resource reference that can be read by the client", + inputSchema: GetResourceReferenceSchema.shape, + }, + async (args, extra) => { + const resourceId = args.resourceId; const resourceIndex = resourceId - 1; if (resourceIndex < 0 || resourceIndex >= ALL_RESOURCES.length) { @@ -697,7 +698,7 @@ export const createServer = () => { }, { type: "resource" as const, - resource: resource, + resource: resource as any, }, { type: "text" as const, @@ -706,12 +707,19 @@ export const createServer = () => { ], }; } + ); - if (name === ToolName.LONG_RUNNING_OPERATION) { - const validatedArgs = LongRunningOperationSchema.parse(args); - const { duration, steps } = validatedArgs; + server.registerTool( + ToolName.LONG_RUNNING_OPERATION, + { + title: "Long Running Operation", + description: "Demonstrates a long running operation with progress notifications", + inputSchema: LongRunningOperationSchema.shape, + }, + async (args, extra) => { + const { duration, steps } = args; const stepDuration = duration / steps; - const progressToken = request.params._meta?.progressToken; + const progressToken = extra._meta?.progressToken; for (let i = 1; i < steps + 1; i++) { await new Promise((resolve) => @@ -739,10 +747,17 @@ export const createServer = () => { ], }; } + ); - if (name === ToolName.SAMPLE_LLM) { - const validatedArgs = SampleLLMSchema.parse(args); - const { prompt, maxTokens } = validatedArgs; + server.registerTool( + ToolName.SAMPLE_LLM, + { + title: "Sample LLM", + description: "Requests the client to sample from an LLM", + inputSchema: SampleLLMSchema.shape, + }, + async (args, extra) => { + const { prompt, maxTokens } = args; const result = await requestSampling( prompt, @@ -756,10 +771,16 @@ export const createServer = () => { ], }; } + ); - if (name === ToolName.ELICITATION) { - ElicitationSchema.parse(args); - + server.registerTool( + ToolName.ELICITATION, + { + title: "Start Elicitation", + description: "Demonstrates client-side user input collection via elicitation", + inputSchema: ElicitationSchema.shape, + }, + async (args, extra) => { const elicitationResult = await extra.sendRequest({ method: 'elicitation/create', params: { @@ -876,10 +897,16 @@ export const createServer = () => { return { content }; } + ); - if (name === ToolName.LIST_ROOTS) { - ListRootsSchema.parse(args); - + server.registerTool( + ToolName.LIST_ROOTS, + { + title: "List Roots", + description: "Lists the root directories provided by the MCP client", + inputSchema: ListRootsSchema.shape, + }, + async (args, extra) => { if (!clientSupportsRoots) { return { content: [ @@ -922,10 +949,7 @@ export const createServer = () => { ] }; } - - // If we get here, the tool was registered with registerTool and will be handled automatically - throw new Error(`Tool ${name} should be handled by registerTool or is unknown`); - }); + ); server.server.setRequestHandler(CompleteRequestSchema, async (request) => { const { ref, argument } = request.params; @@ -965,14 +989,14 @@ export const createServer = () => { currentRoots = response.roots; // Log the roots update for demonstration - await server.server.sendLoggingMessage({ + await server.sendLoggingMessage({ level: "info", logger: "everything-server", data: `Roots updated: ${currentRoots.length} root(s) received from client`, }); } } catch (error) { - await server.server.sendLoggingMessage({ + await server.sendLoggingMessage({ level: "error", logger: "everything-server", data: `Failed to request roots from client: ${error instanceof Error ? error.message : String(error)}`, @@ -991,27 +1015,27 @@ export const createServer = () => { if (response && 'roots' in response) { currentRoots = response.roots; - await server.server.sendLoggingMessage({ + await server.sendLoggingMessage({ level: "info", logger: "everything-server", data: `Initial roots received: ${currentRoots.length} root(s) from client`, }); } else { - await server.server.sendLoggingMessage({ + await server.sendLoggingMessage({ level: "warning", logger: "everything-server", data: "Client returned no roots set", }); } } catch (error) { - await server.server.sendLoggingMessage({ + await server.sendLoggingMessage({ level: "error", logger: "everything-server", data: `Failed to request initial roots from client: ${error instanceof Error ? error.message : String(error)}`, }); } } else { - await server.server.sendLoggingMessage({ + await server.sendLoggingMessage({ level: "info", logger: "everything-server", data: "Client does not support MCP roots protocol", diff --git a/src/everything/sse.ts b/src/everything/sse.ts index f8553aad37..724434f471 100644 --- a/src/everything/sse.ts +++ b/src/everything/sse.ts @@ -28,14 +28,14 @@ app.get("/sse", async (req, res) => { transports.set(transport.sessionId, transport); // Connect server to transport - await server.server.connect(transport); + await server.connect(transport); console.error("Client Connected: ", transport.sessionId); // Start notification intervals after client connects startNotificationIntervals(transport.sessionId); // Handle close of connection - server.server.onclose = async () => { + transport.onclose = async () => { console.error("Client Disconnected: ", transport.sessionId); transports.delete(transport.sessionId); await cleanup(); diff --git a/src/everything/stdio.ts b/src/everything/stdio.ts index e4a04d0751..e443a983b6 100644 --- a/src/everything/stdio.ts +++ b/src/everything/stdio.ts @@ -9,13 +9,13 @@ async function main() { const transport = new StdioServerTransport(); const {server, cleanup, startNotificationIntervals} = createServer(); - await server.server.connect(transport); + await server.connect(transport); startNotificationIntervals(); // Cleanup on exit process.on("SIGINT", async () => { await cleanup(); - await server.server.close(); + await server.close(); process.exit(0); }); } diff --git a/src/everything/streamableHttp.ts b/src/everything/streamableHttp.ts index 7998356de4..18b164c959 100644 --- a/src/everything/streamableHttp.ts +++ b/src/everything/streamableHttp.ts @@ -52,7 +52,7 @@ app.post('/mcp', async (req: Request, res: Response) => { // Set up onclose handler to clean up transport when closed - server.server.onclose = async () => { + transport.onclose = async () => { const sid = transport.sessionId; if (sid && transports.has(sid)) { console.error(`Transport closed for session ${sid}, removing from transports map`); @@ -63,7 +63,7 @@ app.post('/mcp', async (req: Request, res: Response) => { // Connect the transport to the MCP server BEFORE handling the request // so responses can flow back through the same transport - await server.server.connect(transport); + await server.connect(transport); await transport.handleRequest(req, res); From 07a8809522b17170300becdfbc05a2d3ab02cf6d Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Mon, 17 Nov 2025 15:30:39 +0000 Subject: [PATCH 3/4] fix: convert resources to use registerResource() API - Remove manual resource request handlers - Use ResourceTemplate for dynamic resources - Remove pagination logic (not needed with templates) - Keep subscribe/unsubscribe as setRequestHandler (no modern API alternative) - Properly handle both text and blob resource types --- src/everything/everything.ts | 89 +++++++++++++----------------------- 1 file changed, 32 insertions(+), 57 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index d33b2e0391..1ce15ee9b7 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -1,4 +1,4 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { CallToolRequestSchema, @@ -8,10 +8,7 @@ import { CreateMessageResultSchema, ElicitResultSchema, GetPromptRequestSchema, - ListResourcesRequestSchema, - ListResourceTemplatesRequestSchema, LoggingLevel, - ReadResourceRequestSchema, Resource, RootsListChangedNotificationSchema, ServerNotification, @@ -260,64 +257,42 @@ export const createServer = () => { } }); - const PAGE_SIZE = 10; - - // Initialize resource handlers to set up capabilities - // We'll override them below with our custom pagination logic - server['setResourceRequestHandlers'](); - - server.server.setRequestHandler(ListResourcesRequestSchema, async (request) => { - const cursor = request.params?.cursor; - let startIndex = 0; - - if (cursor) { - const decodedCursor = parseInt(atob(cursor), 10); - if (!isNaN(decodedCursor)) { - startIndex = decodedCursor; - } - } - - const endIndex = Math.min(startIndex + PAGE_SIZE, ALL_RESOURCES.length); - const resources = ALL_RESOURCES.slice(startIndex, endIndex); - - let nextCursor: string | undefined; - if (endIndex < ALL_RESOURCES.length) { - nextCursor = btoa(endIndex.toString()); - } - - return { - resources, - nextCursor, - }; - }); - - server.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { - return { - resourceTemplates: [ - { - uriTemplate: "test://static/resource/{id}", - name: "Static Resource", - description: "A static resource with a numeric ID", - }, - ], - }; - }); - - server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - const uri = request.params.uri; + // Register resource using modern API with a template pattern + server.registerResource( + 'static-resources', + new ResourceTemplate('test://static/resource/{id}', { list: undefined }), + { + description: 'A static test resource with a numeric ID', + }, + async (uri, variables, extra) => { + const id = Array.isArray(variables.id) ? variables.id[0] : variables.id; + const index = parseInt(id, 10) - 1; - if (uri.startsWith("test://static/resource/")) { - const index = parseInt(uri.split("/").pop() ?? "", 10) - 1; if (index >= 0 && index < ALL_RESOURCES.length) { const resource = ALL_RESOURCES[index]; - return { - contents: [resource], - }; + // Build the resource contents based on type + if ('text' in resource && resource.text) { + return { + contents: [{ + uri: resource.uri, + mimeType: resource.mimeType, + text: resource.text as string, + }], + }; + } else if ('blob' in resource && resource.blob) { + return { + contents: [{ + uri: resource.uri, + mimeType: resource.mimeType, + blob: resource.blob as string, + }], + }; + } } - } - throw new Error(`Unknown resource: ${uri}`); - }); + throw new Error(`Unknown resource: ${uri.href}`); + } + ); server.server.setRequestHandler(SubscribeRequestSchema, async (request, extra) => { const { uri } = request.params; From f0fc52cdc9ba2c00b5c488773071af9e95719aa5 Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Mon, 17 Nov 2025 16:06:37 +0000 Subject: [PATCH 4/4] fix: register all resources individually for listings Resources are now registered individually so they appear in resources/list responses, while also keeping the template for dynamic access via test://static/resource/{id}. --- src/everything/everything.ts | 43 +++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 1ce15ee9b7..89da0cb03d 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -257,14 +257,51 @@ export const createServer = () => { } }); - // Register resource using modern API with a template pattern + // Register all 100 resources individually so they appear in listings + for (let i = 0; i < ALL_RESOURCES.length; i++) { + const resource = ALL_RESOURCES[i]; + const resourceId = i + 1; + + server.registerResource( + `static-resource-${resourceId}`, + resource.uri, + { + name: resource.name, + description: `Resource ${resourceId}: ${resource.mimeType === "text/plain" ? "plaintext resource" : "binary blob resource"}`, + mimeType: resource.mimeType, + }, + async () => { + // Build the resource contents based on type + if ('text' in resource && resource.text) { + return { + contents: [{ + uri: resource.uri, + mimeType: resource.mimeType, + text: resource.text as string, + }], + }; + } else if ('blob' in resource && resource.blob) { + return { + contents: [{ + uri: resource.uri, + mimeType: resource.mimeType, + blob: resource.blob as string, + }], + }; + } + throw new Error(`Resource ${resource.uri} has neither text nor blob`); + } + ); + } + + // Also register a template for the same pattern to support dynamic access server.registerResource( - 'static-resources', + 'static-resources-template', new ResourceTemplate('test://static/resource/{id}', { list: undefined }), { description: 'A static test resource with a numeric ID', }, - async (uri, variables, extra) => { + async (uri, variables) => { const id = Array.isArray(variables.id) ? variables.id[0] : variables.id; const index = parseInt(id, 10) - 1;