From a83f08609fc5a060213c554de8e2c356cb97cb65 Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Mon, 29 Sep 2025 16:57:59 +0000 Subject: [PATCH 1/3] docs: make table of contents collapsible and improve quick start - Wrap TOC in collapsible details/summary tags to reduce initial scroll - Update quick start example to use streamable HTTP instead of stdio - Reorder overview to emphasize servers over clients - Move resources section after tools since tools are more commonly used - Add structured output examples to tool responses - Improve code examples with better formatting and comments - Add direct links to MCP clients (Inspector, Claude Code, VS Code, Cursor) - Clarify tool vs resource use cases - Remove redundant 'What is MCP?' section - Simplify stateless HTTP example with clearer comments --- README.md | 359 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 197 insertions(+), 162 deletions(-) diff --git a/README.md b/README.md index 43b62ac60..bbee6cb0b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # MCP TypeScript SDK ![NPM Version](https://img.shields.io/npm/v/%40modelcontextprotocol%2Fsdk) ![MIT licensed](https://img.shields.io/npm/l/%40modelcontextprotocol%2Fsdk) -## Table of Contents +
+Table of Contents - [Overview](#overview) - [Installation](#installation) @@ -30,14 +31,15 @@ - [Contributing](#contributing) - [License](#license) +
+ ## Overview -The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements the full MCP specification, making it easy to: +The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements [the full MCP specification](https://modelcontextprotocol.io/specification/latest), making it easy to: -- Build MCP clients that can connect to any MCP server - Create MCP servers that expose resources, prompts and tools +- Build MCP clients that can connect to any MCP server - Use standard transports like stdio and Streamable HTTP -- Handle all MCP protocol messages and lifecycle events ## Installation @@ -45,15 +47,14 @@ The Model Context Protocol allows applications to provide context for LLMs in a npm install @modelcontextprotocol/sdk ``` -> ⚠️ MCP requires Node.js v18.x or higher to work fine. - ## Quick Start -Let's create a simple MCP server that exposes a calculator tool and some data: +Let's create a simple MCP server that exposes a calculator tool and some data. Save the following as `server.mjs`: -```typescript +```javascript import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import express from "express"; import { z } from "zod"; // Create an MCP server @@ -67,18 +68,23 @@ server.registerTool("add", { title: "Addition Tool", description: "Add two numbers", - inputSchema: { a: z.number(), b: z.number() } + inputSchema: { a: z.number(), b: z.number() }, + outputSchema: { result: z.number() } }, - async ({ a, b }) => ({ - content: [{ type: "text", text: String(a + b) }] - }) + async ({ a, b }) => { + const output = { result: a + b }; + return { + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output + }; + } ); // Add a dynamic greeting resource server.registerResource( "greeting", new ResourceTemplate("greeting://{name}", { list: undefined }), - { + { title: "Greeting Resource", // Display name for UI description: "Dynamic greeting generator" }, @@ -90,19 +96,41 @@ server.registerResource( }) ); -// Start receiving messages on stdin and sending messages on stdout -const transport = new StdioServerTransport(); -await server.connect(transport); -``` +// Set up Express and HTTP transport +const app = express(); +app.use(express.json()); -## What is MCP? +app.post("/mcp", async (req, res) => { + // Create a new transport for each request to prevent request ID collisions + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + + res.on('close', () => { + transport.close(); + }); -The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can: + await server.connect(transport); + await transport.handleRequest(req, res, req.body); +}); -- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context) -- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) -- Define interaction patterns through **Prompts** (reusable templates for LLM interactions) -- And more! +const port = parseInt(process.env.PORT || "3000"); +app.listen(port, () => { + console.log(`Demo MCP Server running on http://localhost:${port}/mcp`); +}).on("error", (error) => { + console.error("Server error:", error); + process.exit(1); +}); +``` + +Now run the server with `node server.mjs`. You can connect to it using any MCP client that supports streamable http, such as: +- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): `npx npx @modelcontextprotocol/inspector` and connect to the streamable HTTP URL `http://localhost:3000/mcp` +- [Claude Code](https://docs.claude.com/en/docs/claude-code/mcp): `claude mcp add --transport http my-server http://localhost:3000/mcp` +- [VS Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers): `code --add-mcp "{\"name\":\"my-server\",\"type\":\"http\",\"url\":\"http://localhost:3000/mcp\"}"` +- [Cursor](https://cursor.com/docs/context/mcp): Click [this deeplink](cursor://anysphere.cursor-deeplink/mcp/install?name=my-server&config=eyJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvbWNwIn0%3D) + +Then try asking your agent to add two numbers using its new tool! ## Core Concepts @@ -117,75 +145,9 @@ const server = new McpServer({ }); ``` -### Resources - -Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects: - -```typescript -// Static resource -server.registerResource( - "config", - "config://app", - { - title: "Application Config", - description: "Application configuration data", - mimeType: "text/plain" - }, - async (uri) => ({ - contents: [{ - uri: uri.href, - text: "App configuration here" - }] - }) -); - -// Dynamic resource with parameters -server.registerResource( - "user-profile", - new ResourceTemplate("users://{userId}/profile", { list: undefined }), - { - title: "User Profile", - description: "User profile information" - }, - async (uri, { userId }) => ({ - contents: [{ - uri: uri.href, - text: `Profile data for user ${userId}` - }] - }) -); - -// Resource with context-aware completion -server.registerResource( - "repository", - new ResourceTemplate("github://repos/{owner}/{repo}", { - list: undefined, - complete: { - // Provide intelligent completions based on previously resolved parameters - repo: (value, context) => { - if (context?.arguments?.["owner"] === "org1") { - return ["project1", "project2", "project3"].filter(r => r.startsWith(value)); - } - return ["default-repo"].filter(r => r.startsWith(value)); - } - } - }), - { - title: "GitHub Repository", - description: "Repository information" - }, - async (uri, { owner, repo }) => ({ - contents: [{ - uri: uri.href, - text: `Repository: ${owner}/${repo}` - }] - }) -); -``` - ### Tools -Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects: +[Tools](https://modelcontextprotocol.io/specification/latest/server/tools) let LLMs take actions through your server. Tools can perform computation, fetch data and have side effects. Tools should be designed to be model-controlled - i.e. AI models will decide which tools to call, and the arguments. ```typescript // Simple tool with parameters @@ -197,14 +159,19 @@ server.registerTool( inputSchema: { weightKg: z.number(), heightM: z.number() - } + }, + outputSchema: { bmi: z.number() } }, - async ({ weightKg, heightM }) => ({ - content: [{ - type: "text", - text: String(weightKg / (heightM * heightM)) - }] - }) + async ({ weightKg, heightM }) => { + const output = { bmi: weightKg / (heightM * heightM) }; + return { + content: [{ + type: "text", + text: JSON.stringify(output) + }], + structuredContent: output + }; + } ); // Async tool with external API call @@ -257,11 +224,79 @@ server.registerTool( #### ResourceLinks -Tools can return `ResourceLink` objects to reference resources without embedding their full content. This is essential for performance when dealing with large files or many resources - clients can then selectively read only the resources they need using the provided URIs. +Tools can return `ResourceLink` objects to reference resources without embedding their full content. This can be helpful for performance when dealing with large files or many resources - clients can then selectively read only the resources they need using the provided URIs. + +### Resources + +[Resources](https://modelcontextprotocol.io/specification/latest/server/resources) can also expose data to LLMs, but unlike tools shouldn't perform significant computation or have side effects. + +Resources are designed to be used in an application-driven way, meaning MCP client applications can decide how to expose them. For example, a client could expose a resource picker to the human, or could expose them to the model directly. + +```typescript +// Static resource +server.registerResource( + "config", + "config://app", + { + title: "Application Config", + description: "Application configuration data", + mimeType: "text/plain" + }, + async (uri) => ({ + contents: [{ + uri: uri.href, + text: "App configuration here" + }] + }) +); + +// Dynamic resource with parameters +server.registerResource( + "user-profile", + new ResourceTemplate("users://{userId}/profile", { list: undefined }), + { + title: "User Profile", + description: "User profile information" + }, + async (uri, { userId }) => ({ + contents: [{ + uri: uri.href, + text: `Profile data for user ${userId}` + }] + }) +); + +// Resource with context-aware completion +server.registerResource( + "repository", + new ResourceTemplate("github://repos/{owner}/{repo}", { + list: undefined, + complete: { + // Provide intelligent completions based on previously resolved parameters + repo: (value, context) => { + if (context?.arguments?.["owner"] === "org1") { + return ["project1", "project2", "project3"].filter(r => r.startsWith(value)); + } + return ["default-repo"].filter(r => r.startsWith(value)); + } + } + }), + { + title: "GitHub Repository", + description: "Repository information" + }, + async (uri, { owner, repo }) => ({ + contents: [{ + uri: uri.href, + text: `Repository: ${owner}/${repo}` + }] + }) +); +``` ### Prompts -Prompts are reusable templates that help LLMs interact with your server effectively: +[Prompts](https://modelcontextprotocol.io/specification/latest/server/prompts) are reusable templates that help humans prompt models to interact with your server. They're designed to be user-driven, and might appear as slash commands in a chat interface. ```typescript import { completable } from "@modelcontextprotocol/sdk/server/completable.js"; @@ -349,7 +384,7 @@ const result = await client.complete({ ### Display Names and Metadata -All resources, tools, and prompts support an optional `title` field for better UI presentation. The `title` is used as a display name, while `name` remains the unique identifier. +All resources, tools, and prompts support an optional `title` field for better UI presentation. The `title` is used as a display name (e.g. 'Create a new issue'), while `name` remains the unique identifier (e.g. `create_issue`). **Note:** The `register*` methods (`registerTool`, `registerPrompt`, `registerResource`) are the recommended approach for new code. The older methods (`tool`, `prompt`, `resource`) remain available for backwards compatibility. @@ -599,24 +634,54 @@ This configuration is necessary because: For simpler use cases where session management isn't needed: ```typescript +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import express from "express"; +import { z } from "zod"; + const app = express(); app.use(express.json()); -app.post('/mcp', async (req: Request, res: Response) => { - // In stateless mode, create a new instance of transport and server for each request - // to ensure complete isolation. A single instance would cause request ID collisions - // when multiple clients connect concurrently. - +// Create the MCP server once (can be reused across requests) +const server = new McpServer({ + name: 'example-server', + version: '1.0.0', +}); + +// Set up your tools, resources, and prompts +server.registerTool( + "echo", + { + title: "Echo Tool", + description: "Echoes back the provided message", + inputSchema: { message: z.string() }, + outputSchema: { echo: z.string() } + }, + async ({ message }) => { + const output = { echo: `Tool echo: ${message}` }; + return { + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output + }; + } +); + +app.post('/mcp', async (req, res) => { + // In stateless mode, create a new transport for each request to prevent + // request ID collisions. Different clients may use the same JSON-RPC request IDs, + // which would cause responses to be routed to the wrong HTTP connections if + // the transport state is shared. + try { - const server = getServer(); - const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, + enableJsonResponse: true, }); + res.on('close', () => { - console.log('Request closed'); transport.close(); - server.close(); }); + await server.connect(transport); await transport.handleRequest(req, res, req.body); } catch (error) { @@ -634,48 +699,13 @@ app.post('/mcp', async (req: Request, res: Response) => { } }); -// SSE notifications not supported in stateless mode -app.get('/mcp', async (req: Request, res: Response) => { - console.log('Received GET MCP request'); - res.writeHead(405).end(JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Method not allowed." - }, - id: null - })); -}); - -// Session termination not needed in stateless mode -app.delete('/mcp', async (req: Request, res: Response) => { - console.log('Received DELETE MCP request'); - res.writeHead(405).end(JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Method not allowed." - }, - id: null - })); -}); - - -// Start the server -const PORT = 3000; -setupServer().then(() => { - app.listen(PORT, (error) => { - if (error) { - console.error('Failed to start server:', error); - process.exit(1); - } - console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`); - }); -}).catch(error => { - console.error('Failed to set up the server:', error); +const port = parseInt(process.env.PORT || "3000"); +app.listen(port, () => { + console.log(`Demo MCP Server running on http://localhost:${port}/mcp`); +}).on("error", (error) => { + console.error("Server error:", error); process.exit(1); }); - ``` This stateless approach is useful for: @@ -719,6 +749,23 @@ const server = new McpServer({ version: "1.0.0" }); +server.registerTool( + "echo", + { + title: "Echo Tool", + description: "Echoes back the provided message", + inputSchema: { message: z.string() }, + outputSchema: { echo: z.string() } + }, + async ({ message }) => { + const output = { echo: `Tool echo: ${message}` }; + return { + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output + }; + } +); + server.registerResource( "echo", new ResourceTemplate("echo://{message}", { list: undefined }), @@ -734,18 +781,6 @@ server.registerResource( }) ); -server.registerTool( - "echo", - { - title: "Echo Tool", - description: "Echoes back the provided message", - inputSchema: { message: z.string() } - }, - async ({ message }) => ({ - content: [{ type: "text", text: `Tool echo: ${message}` }] - }) -); - server.registerPrompt( "echo", { From 996fc0bf857eedf3944f7af36d36cab8e25d15db Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Mon, 29 Sep 2025 17:39:55 +0000 Subject: [PATCH 2/3] refactor: update example servers to use registerTool with outputSchema - Replace .tool() with .registerTool() in all example servers - Add outputSchema to all tool definitions - Add structuredContent to all tool responses - Align text content with structured output using JSON.stringify - Add title fields to all tools for better UI presentation --- README.md | 464 +++++++++++------- .../server/jsonResponseStreamableHttp.ts | 32 +- src/examples/server/simpleStreamableHttp.ts | 106 +++- .../sseAndStreamableHttpCompatibleServer.ts | 24 +- 4 files changed, 410 insertions(+), 216 deletions(-) diff --git a/README.md b/README.md index bbee6cb0b..864c599cb 100644 --- a/README.md +++ b/README.md @@ -180,13 +180,16 @@ server.registerTool( { title: "Weather Fetcher", description: "Get weather data for a city", - inputSchema: { city: z.string() } + inputSchema: { city: z.string() }, + outputSchema: { temperature: z.number(), conditions: z.string() } }, async ({ city }) => { const response = await fetch(`https://api.weather.com/${city}`); - const data = await response.text(); + const data = await response.json(); + const output = { temperature: data.temp, conditions: data.conditions }; return { - content: [{ type: "text", text: data }] + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output }; } ); @@ -197,28 +200,42 @@ server.registerTool( { title: "List Files", description: "List project files", - inputSchema: { pattern: z.string() } + inputSchema: { pattern: z.string() }, + outputSchema: { + count: z.number(), + files: z.array(z.object({ name: z.string(), uri: z.string() })) + } }, - async ({ pattern }) => ({ - content: [ - { type: "text", text: `Found files matching "${pattern}":` }, - // ResourceLinks let tools return references without file content - { - type: "resource_link", - uri: "file:///project/README.md", - name: "README.md", - mimeType: "text/markdown", - description: 'A README file' - }, - { - type: "resource_link", - uri: "file:///project/src/index.ts", - name: "index.ts", - mimeType: "text/typescript", - description: 'An index file' - } - ] - }) + async ({ pattern }) => { + const output = { + count: 2, + files: [ + { name: "README.md", uri: "file:///project/README.md" }, + { name: "index.ts", uri: "file:///project/src/index.ts" } + ] + }; + return { + content: [ + { type: "text", text: JSON.stringify(output) }, + // ResourceLinks let tools return references without file content + { + type: "resource_link", + uri: "file:///project/README.md", + name: "README.md", + mimeType: "text/markdown", + description: 'A README file' + }, + { + type: "resource_link", + uri: "file:///project/src/index.ts", + name: "index.ts", + mimeType: "text/typescript", + description: 'An index file' + } + ], + structuredContent: output + }; + } ); ``` @@ -426,7 +443,8 @@ MCP servers can request LLM completions from connected clients that support samp ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import express from "express"; import { z } from "zod"; const mcpServer = new McpServer({ @@ -438,10 +456,12 @@ const mcpServer = new McpServer({ mcpServer.registerTool( "summarize", { + title: "Text Summarizer", description: "Summarize any text using an LLM", inputSchema: { text: z.string().describe("Text to summarize"), }, + outputSchema: { summary: z.string() } }, async ({ text }) => { // Call the LLM through MCP sampling @@ -458,24 +478,36 @@ mcpServer.registerTool( maxTokens: 500, }); + const summary = response.content.type === "text" ? response.content.text : "Unable to generate summary"; + const output = { summary }; return { - content: [ - { - type: "text", - text: response.content.type === "text" ? response.content.text : "Unable to generate summary", - }, - ], + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output }; } ); -async function main() { - const transport = new StdioServerTransport(); +const app = express(); +app.use(express.json()); + +app.post("/mcp", async (req, res) => { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + + res.on('close', () => { + transport.close(); + }); + await mcpServer.connect(transport); - console.error("MCP server is running..."); -} + await transport.handleRequest(req, res, req.body); +}); -main().catch((error) => { +const port = parseInt(process.env.PORT || "3000"); +app.listen(port, () => { + console.log(`MCP Server running on http://localhost:${port}/mcp`); +}).on("error", (error) => { console.error("Server error:", error); process.exit(1); }); @@ -486,32 +518,92 @@ main().catch((error) => { MCP servers in TypeScript need to be connected to a transport to communicate with clients. How you start the server depends on the choice of transport: -### stdio +### Streamable HTTP + +For remote servers, use the Streamable HTTP transport. -For command-line tools and direct integrations: +#### Without Session Management (Recommended) + +For most use cases where session management isn't needed: ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import express from "express"; +import { z } from "zod"; +const app = express(); +app.use(express.json()); + +// Create the MCP server once (can be reused across requests) const server = new McpServer({ - name: "example-server", - version: "1.0.0" + name: 'example-server', + version: '1.0.0', }); -// ... set up server resources, tools, and prompts ... +// Set up your tools, resources, and prompts +server.registerTool( + "echo", + { + title: "Echo Tool", + description: "Echoes back the provided message", + inputSchema: { message: z.string() }, + outputSchema: { echo: z.string() } + }, + async ({ message }) => { + const output = { echo: `Tool echo: ${message}` }; + return { + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output + }; + } +); -const transport = new StdioServerTransport(); -await server.connect(transport); -``` +app.post('/mcp', async (req, res) => { + // In stateless mode, create a new transport for each request to prevent + // request ID collisions. Different clients may use the same JSON-RPC request IDs, + // which would cause responses to be routed to the wrong HTTP connections if + // the transport state is shared. -### Streamable HTTP + try { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + + res.on('close', () => { + transport.close(); + }); + + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } +}); -For remote servers, set up a Streamable HTTP transport that handles both client requests and server-to-client notifications. +const port = parseInt(process.env.PORT || "3000"); +app.listen(port, () => { + console.log(`MCP Server running on http://localhost:${port}/mcp`); +}).on("error", (error) => { + console.error("Server error:", error); + process.exit(1); +}); +``` #### With Session Management -In some cases, servers need to be stateful. This is achieved by [session management](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management). +In some cases, servers need stateful sessions. This can be achieved by [session management](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management) in the MCP protocol. ```typescript import express from "express"; @@ -520,8 +612,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js" - - const app = express(); app.use(express.json()); @@ -604,10 +694,6 @@ app.delete('/mcp', handleSessionRequest); app.listen(3000); ``` -> [!TIP] -> When using this in a remote environment, make sure to allow the header parameter `mcp-session-id` in CORS. Otherwise, it may result in a `Bad Request: No valid session ID provided` error. Read the following section for examples. - - #### CORS Configuration for Browser-Based Clients If you'd like your server to be accessible by browser-based MCP clients, you'll need to configure CORS headers. The `Mcp-Session-Id` header must be exposed for browser clients to access it: @@ -629,91 +715,6 @@ This configuration is necessary because: - Browsers restrict access to response headers unless explicitly exposed via CORS - Without this configuration, browser-based clients won't be able to read the session ID from initialization responses -#### Without Session Management (Stateless) - -For simpler use cases where session management isn't needed: - -```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import express from "express"; -import { z } from "zod"; - -const app = express(); -app.use(express.json()); - -// Create the MCP server once (can be reused across requests) -const server = new McpServer({ - name: 'example-server', - version: '1.0.0', -}); - -// Set up your tools, resources, and prompts -server.registerTool( - "echo", - { - title: "Echo Tool", - description: "Echoes back the provided message", - inputSchema: { message: z.string() }, - outputSchema: { echo: z.string() } - }, - async ({ message }) => { - const output = { echo: `Tool echo: ${message}` }; - return { - content: [{ type: "text", text: JSON.stringify(output) }], - structuredContent: output - }; - } -); - -app.post('/mcp', async (req, res) => { - // In stateless mode, create a new transport for each request to prevent - // request ID collisions. Different clients may use the same JSON-RPC request IDs, - // which would cause responses to be routed to the wrong HTTP connections if - // the transport state is shared. - - try { - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - enableJsonResponse: true, - }); - - res.on('close', () => { - transport.close(); - }); - - await server.connect(transport); - await transport.handleRequest(req, res, req.body); - } catch (error) { - console.error('Error handling MCP request:', error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal server error', - }, - id: null, - }); - } - } -}); - -const port = parseInt(process.env.PORT || "3000"); -app.listen(port, () => { - console.log(`Demo MCP Server running on http://localhost:${port}/mcp`); -}).on("error", (error) => { - console.error("Server error:", error); - process.exit(1); -}); -``` - -This stateless approach is useful for: - -- Simple API wrappers -- RESTful scenarios where each request is independent -- Horizontally scaled deployments without shared session state - #### DNS Rebinding Protection The Streamable HTTP transport includes DNS rebinding protection to prevent security vulnerabilities. By default, this protection is **disabled** for backwards compatibility. @@ -730,6 +731,25 @@ const transport = new StreamableHTTPServerTransport({ }); ``` +### stdio + +For local integrations spawned by another process, you can use the stdio transport: + +```typescript +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + +const server = new McpServer({ + name: "example-server", + version: "1.0.0" +}); + +// ... set up server resources, tools, and prompts ... + +const transport = new StdioServerTransport(); +await server.connect(transport); +``` + ### Testing and Debugging To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information. @@ -855,17 +875,23 @@ server.registerTool( { title: "SQL Query", description: "Execute SQL queries on the database", - inputSchema: { sql: z.string() } + inputSchema: { sql: z.string() }, + outputSchema: { + rows: z.array(z.record(z.any())), + rowCount: z.number() + } }, async ({ sql }) => { const db = getDb(); try { const results = await db.all(sql); + const output = { rows: results, rowCount: results.length }; return { content: [{ type: "text", - text: JSON.stringify(results, null, 2) - }] + text: JSON.stringify(output, null, 2) + }], + structuredContent: output }; } catch (err: unknown) { const error = err as Error; @@ -889,8 +915,10 @@ server.registerTool( If you want to offer an initial set of tools/prompts/resources, but later add additional ones based on user action or external state change, you can add/update/remove them _after_ the Server is connected. This will automatically emit the corresponding `listChanged` notifications: -```ts +```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import express from "express"; import { z } from "zod"; const server = new McpServer({ @@ -898,31 +926,64 @@ const server = new McpServer({ version: "1.0.0" }); -const listMessageTool = server.tool( +const listMessageTool = server.registerTool( "listMessages", - { channel: z.string() }, - async ({ channel }) => ({ - content: [{ type: "text", text: await listMessages(channel) }] - }) + { + title: "List Messages", + description: "List messages in a channel", + inputSchema: { channel: z.string() }, + outputSchema: { messages: z.array(z.string()) } + }, + async ({ channel }) => { + const messages = await listMessages(channel); + const output = { messages }; + return { + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output + }; + } ); -const putMessageTool = server.tool( +const putMessageTool = server.registerTool( "putMessage", - { channel: z.string(), message: z.string() }, - async ({ channel, message }) => ({ - content: [{ type: "text", text: await putMessage(channel, message) }] - }) + { + title: "Put Message", + description: "Send a message to a channel", + inputSchema: { channel: z.string(), message: z.string() }, + outputSchema: { success: z.boolean() } + }, + async ({ channel, message }) => { + await putMessage(channel, message); + const output = { success: true }; + return { + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output + }; + } ); // Until we upgrade auth, `putMessage` is disabled (won't show up in listTools) putMessageTool.disable() -const upgradeAuthTool = server.tool( +const upgradeAuthTool = server.registerTool( "upgradeAuth", - { permission: z.enum(["write", "admin"])}, + { + title: "Upgrade Authorization", + description: "Upgrade user authorization level", + inputSchema: { permission: z.enum(["write", "admin"]) }, + outputSchema: { + success: z.boolean(), + newPermission: z.string() + } + }, // Any mutations here will automatically emit `listChanged` notifications async ({ permission }) => { const { ok, err, previous } = await upgradeAuthAndStoreToken(permission) - if (!ok) return {content: [{ type: "text", text: `Error: ${err}` }]} + if (!ok) { + return { + content: [{ type: "text", text: `Error: ${err}` }], + isError: true + }; + } // If we previously had read-only access, 'putMessage' is now available if (previous === "read") { @@ -930,8 +991,8 @@ const upgradeAuthTool = server.tool( } if (permission === 'write') { - // If we've just upgraded to 'write' permissions, we can still call 'upgradeAuth' - // but can only upgrade to 'admin'. + // If we've just upgraded to 'write' permissions, we can still call 'upgradeAuth' + // but can only upgrade to 'admin'. upgradeAuthTool.update({ paramsSchema: { permission: z.enum(["admin"]) }, // change validation rules }) @@ -939,12 +1000,37 @@ const upgradeAuthTool = server.tool( // If we're now an admin, we no longer have anywhere to upgrade to, so fully remove that tool upgradeAuthTool.remove() } + + const output = { success: true, newPermission: permission }; + return { + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output + }; } ) -// Connect as normal -const transport = new StdioServerTransport(); -await server.connect(transport); +// Connect with HTTP transport +const app = express(); +app.use(express.json()); + +app.post('/mcp', async (req, res) => { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + + res.on('close', () => { + transport.close(); + }); + + await server.connect(transport); + await transport.handleRequest(req, res, req.body); +}); + +const port = parseInt(process.env.PORT || "3000"); +app.listen(port, () => { + console.log(`MCP Server running on http://localhost:${port}/mcp`); +}); ``` ### Improving Network Efficiency with Notification Debouncing @@ -1048,17 +1134,30 @@ MCP servers can request additional information from users through the elicitatio ```typescript // Server-side: Restaurant booking tool that asks for alternatives -server.tool( +server.registerTool( "book-restaurant", - { - restaurant: z.string(), - date: z.string(), - partySize: z.number() + { + title: "Book Restaurant", + description: "Book a table at a restaurant", + inputSchema: { + restaurant: z.string(), + date: z.string(), + partySize: z.number() + }, + outputSchema: { + success: z.boolean(), + booking: z.object({ + restaurant: z.string(), + date: z.string(), + partySize: z.number() + }).optional(), + alternatives: z.array(z.string()).optional() + } }, async ({ restaurant, date, partySize }) => { // Check availability const available = await checkAvailability(restaurant, date, partySize); - + if (!available) { // Ask user if they want to try alternative dates const result = await server.server.elicitInput({ @@ -1085,34 +1184,43 @@ server.tool( if (result.action === "accept" && result.content?.checkAlternatives) { const alternatives = await findAlternatives( - restaurant, - date, - partySize, + restaurant, + date, + partySize, result.content.flexibleDates as string ); + const output = { success: false, alternatives }; return { content: [{ type: "text", - text: `Found these alternatives: ${alternatives.join(", ")}` - }] + text: JSON.stringify(output) + }], + structuredContent: output }; } - + + const output = { success: false }; return { content: [{ type: "text", - text: "No booking made. Original date not available." - }] + text: JSON.stringify(output) + }], + structuredContent: output }; } - + // Book the table await makeBooking(restaurant, date, partySize); + const output = { + success: true, + booking: { restaurant, date, partySize } + }; return { content: [{ type: "text", - text: `Booked table for ${partySize} at ${restaurant} on ${date}` - }] + text: JSON.stringify(output) + }], + structuredContent: output }; } ); diff --git a/src/examples/server/jsonResponseStreamableHttp.ts b/src/examples/server/jsonResponseStreamableHttp.ts index bc740c5fa..296bb1c74 100644 --- a/src/examples/server/jsonResponseStreamableHttp.ts +++ b/src/examples/server/jsonResponseStreamableHttp.ts @@ -19,30 +19,44 @@ const getServer = () => { }); // Register a simple tool that returns a greeting - server.tool( + server.registerTool( 'greet', - 'A simple greeting tool', { - name: z.string().describe('Name to greet'), + title: 'Greeting Tool', + description: 'A simple greeting tool', + inputSchema: { + name: z.string().describe('Name to greet'), + }, + outputSchema: { + greeting: z.string() + } }, async ({ name }): Promise => { + const output = { greeting: `Hello, ${name}!` }; return { content: [ { type: 'text', - text: `Hello, ${name}!`, + text: JSON.stringify(output), }, ], + structuredContent: output }; } ); // Register a tool that sends multiple greetings with notifications - server.tool( + server.registerTool( 'multi-greet', - 'A tool that sends different greetings with delays between them', { - name: z.string().describe('Name to greet'), + title: 'Multiple Greeting Tool', + description: 'A tool that sends different greetings with delays between them', + inputSchema: { + name: z.string().describe('Name to greet'), + }, + outputSchema: { + greeting: z.string() + } }, async ({ name }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); @@ -66,13 +80,15 @@ const getServer = () => { data: `Sending second greeting to ${name}` }, extra.sessionId); + const output = { greeting: `Good morning, ${name}!` }; return { content: [ { type: 'text', - text: `Good morning, ${name}!`, + text: JSON.stringify(output), } ], + structuredContent: output }; } ); diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 6f1e20080..b0afa714c 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -35,30 +35,40 @@ const getServer = () => { inputSchema: { name: z.string().describe('Name to greet'), }, + outputSchema: { + greeting: z.string() + } }, async ({ name }): Promise => { + const output = { greeting: `Hello, ${name}!` }; return { content: [ { type: 'text', - text: `Hello, ${name}!`, + text: JSON.stringify(output), }, ], + structuredContent: output }; } ); // Register a tool that sends multiple greetings with notifications (with annotations) - server.tool( + server.registerTool( 'multi-greet', - 'A tool that sends different greetings with delays between them', - { - name: z.string().describe('Name to greet'), - }, { title: 'Multiple Greeting Tool', - readOnlyHint: true, - openWorldHint: false + description: 'A tool that sends different greetings with delays between them', + inputSchema: { + name: z.string().describe('Name to greet'), + }, + outputSchema: { + greeting: z.string() + }, + annotations: { + readOnlyHint: true, + openWorldHint: false + } }, async ({ name }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); @@ -82,23 +92,32 @@ const getServer = () => { data: `Sending second greeting to ${name}` }, extra.sessionId); + const output = { greeting: `Good morning, ${name}!` }; return { content: [ { type: 'text', - text: `Good morning, ${name}!`, + text: JSON.stringify(output), } ], + structuredContent: output }; } ); // Register a tool that demonstrates elicitation (user input collection) // This creates a closure that captures the server instance - server.tool( + server.registerTool( 'collect-user-info', - 'A tool that collects user information through elicitation', { - infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect'), + title: 'Collect User Info', + description: 'A tool that collects user information through elicitation', + inputSchema: { + infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect'), + }, + outputSchema: { + action: z.enum(['accept', 'decline', 'cancel']), + data: z.record(z.any()).optional() + } }, async ({ infoType }): Promise => { let message: string; @@ -201,32 +220,40 @@ const getServer = () => { requestedSchema, }); + const output = { + action: result.action, + data: result.action === 'accept' ? result.content : undefined + }; + if (result.action === 'accept') { return { content: [ { type: 'text', - text: `Thank you! Collected ${infoType} information: ${JSON.stringify(result.content, null, 2)}`, + text: JSON.stringify(output), }, ], + structuredContent: output }; } else if (result.action === 'decline') { return { content: [ { type: 'text', - text: `No information was collected. User declined ${infoType} information request.`, + text: JSON.stringify(output), }, ], + structuredContent: output }; } else { return { content: [ { type: 'text', - text: `Information collection was cancelled by the user.`, + text: JSON.stringify(output), }, ], + structuredContent: output }; } } catch (error) { @@ -237,6 +264,7 @@ const getServer = () => { text: `Error collecting ${infoType} information: ${error}`, }, ], + isError: true }; } } @@ -268,12 +296,20 @@ const getServer = () => { ); // Register a tool specifically for testing resumability - server.tool( + server.registerTool( 'start-notification-stream', - 'Starts sending periodic notifications for testing resumability', { - interval: z.number().describe('Interval in milliseconds between notifications').default(100), - count: z.number().describe('Number of notifications to send (0 for 100)').default(50), + title: 'Start Notification Stream', + description: 'Starts sending periodic notifications for testing resumability', + inputSchema: { + interval: z.number().describe('Interval in milliseconds between notifications').default(100), + count: z.number().describe('Number of notifications to send (0 for 100)').default(50), + }, + outputSchema: { + started: z.boolean(), + interval: z.number(), + totalNotifications: z.number() + } }, async ({ interval, count }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); @@ -294,13 +330,19 @@ const getServer = () => { await sleep(interval); } + const output = { + started: true, + interval, + totalNotifications: counter + }; return { content: [ { type: 'text', - text: `Started sending periodic notifications every ${interval}ms`, + text: JSON.stringify(output), } ], + structuredContent: output }; } ); @@ -376,6 +418,14 @@ const getServer = () => { inputSchema: { includeDescriptions: z.boolean().optional().describe('Whether to include descriptions in the resource links'), }, + outputSchema: { + count: z.number(), + files: z.array(z.object({ + uri: z.string(), + name: z.string(), + mimeType: z.string() + })) + } }, async ({ includeDescriptions = true }): Promise => { const resourceLinks: ResourceLink[] = [ @@ -402,18 +452,24 @@ const getServer = () => { } ]; + const output = { + count: resourceLinks.length, + files: resourceLinks.map(link => ({ + uri: link.uri, + name: link.name, + mimeType: link.mimeType + })) + }; + return { content: [ { type: 'text', - text: 'Here are the available files as resource links:', + text: JSON.stringify(output), }, ...resourceLinks, - { - type: 'text', - text: '\nYou can read any of these resources using their URI.', - } ], + structuredContent: output }; } ); diff --git a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts index a9d9b63d7..7db38ee38 100644 --- a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts +++ b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts @@ -26,12 +26,20 @@ const getServer = () => { }, { capabilities: { logging: {} } }); // Register a simple tool that sends notifications over time - server.tool( + server.registerTool( 'start-notification-stream', - 'Starts sending periodic notifications for testing resumability', { - interval: z.number().describe('Interval in milliseconds between notifications').default(100), - count: z.number().describe('Number of notifications to send (0 for 100)').default(50), + title: 'Start Notification Stream', + description: 'Starts sending periodic notifications for testing resumability', + inputSchema: { + interval: z.number().describe('Interval in milliseconds between notifications').default(100), + count: z.number().describe('Number of notifications to send (0 for 100)').default(50), + }, + outputSchema: { + started: z.boolean(), + interval: z.number(), + totalNotifications: z.number() + } }, async ({ interval, count }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); @@ -52,13 +60,19 @@ const getServer = () => { await sleep(interval); } + const output = { + started: true, + interval, + totalNotifications: counter + }; return { content: [ { type: 'text', - text: `Started sending periodic notifications every ${interval}ms`, + text: JSON.stringify(output), } ], + structuredContent: output }; } ); From f566bdb40fbd2b1e822165e9d91217141dbccd5d Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Thu, 2 Oct 2025 17:04:26 +0000 Subject: [PATCH 3/3] docs: update quickstart to use TypeScript and tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change the quickstart example from JavaScript (.mjs) to TypeScript (.ts) and use npx tsx to run it directly without requiring a build step. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 864c599cb..3d1931795 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,9 @@ npm install @modelcontextprotocol/sdk ## Quick Start -Let's create a simple MCP server that exposes a calculator tool and some data. Save the following as `server.mjs`: +Let's create a simple MCP server that exposes a calculator tool and some data. Save the following as `server.ts`: -```javascript +```typescript import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import express from "express"; @@ -124,8 +124,10 @@ app.listen(port, () => { }); ``` -Now run the server with `node server.mjs`. You can connect to it using any MCP client that supports streamable http, such as: -- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): `npx npx @modelcontextprotocol/inspector` and connect to the streamable HTTP URL `http://localhost:3000/mcp` +Install the deps with `npm install @modelcontextprotocol/sdk express zod@3`, and run with `npx -y tsx server.ts`. + +You can connect to it using any MCP client that supports streamable http, such as: +- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): `npx @modelcontextprotocol/inspector` and connect to the streamable HTTP URL `http://localhost:3000/mcp` - [Claude Code](https://docs.claude.com/en/docs/claude-code/mcp): `claude mcp add --transport http my-server http://localhost:3000/mcp` - [VS Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers): `code --add-mcp "{\"name\":\"my-server\",\"type\":\"http\",\"url\":\"http://localhost:3000/mcp\"}"` - [Cursor](https://cursor.com/docs/context/mcp): Click [this deeplink](cursor://anysphere.cursor-deeplink/mcp/install?name=my-server&config=eyJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvbWNwIn0%3D)