UNPKG

@modelcontextprotocol/sdk

Version:

Model Context Protocol implementation for TypeScript

1,352 lines (1,141 loc) 41.8 kB
# 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 - [Overview](#overview) - [Installation](#installation) - [Quickstart](#quick-start) - [What is MCP?](#what-is-mcp) - [Core Concepts](#core-concepts) - [Server](#server) - [Resources](#resources) - [Tools](#tools) - [Prompts](#prompts) - [Completions](#completions) - [Sampling](#sampling) - [Running Your Server](#running-your-server) - [stdio](#stdio) - [Streamable HTTP](#streamable-http) - [Testing and Debugging](#testing-and-debugging) - [Examples](#examples) - [Echo Server](#echo-server) - [SQLite Explorer](#sqlite-explorer) - [Advanced Usage](#advanced-usage) - [Dynamic Servers](#dynamic-servers) - [Low-Level Server](#low-level-server) - [Writing MCP Clients](#writing-mcp-clients) - [Proxy Authorization Requests Upstream](#proxy-authorization-requests-upstream) - [Backwards Compatibility](#backwards-compatibility) - [Documentation](#documentation) - [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: - Build MCP clients that can connect to any MCP server - Create MCP servers that expose resources, prompts and tools - Use standard transports like stdio and Streamable HTTP - Handle all MCP protocol messages and lifecycle events ## Installation ```bash 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: ```typescript import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; // Create an MCP server const server = new McpServer({ name: 'demo-server', version: '1.0.0' }); // Add an addition tool server.registerTool( 'add', { title: 'Addition Tool', description: 'Add two numbers', inputSchema: { a: z.number(), b: z.number() } }, async ({ a, b }) => ({ content: [{ type: 'text', text: String(a + b) }] }) ); // 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' }, async (uri, { name }) => ({ contents: [ { uri: uri.href, text: `Hello, ${name}!` } ] }) ); // Start receiving messages on stdin and sending messages on stdout const transport = new StdioServerTransport(); await server.connect(transport); ``` ## What is MCP? 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: - 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! ## Core Concepts ### Server The McpServer is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: ```typescript const server = new McpServer({ name: 'my-app', version: '1.0.0' }); ``` ### 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: ```typescript // Simple tool with parameters server.registerTool( 'calculate-bmi', { title: 'BMI Calculator', description: 'Calculate Body Mass Index', inputSchema: { weightKg: z.number(), heightM: z.number() } }, async ({ weightKg, heightM }) => ({ content: [ { type: 'text', text: String(weightKg / (heightM * heightM)) } ] }) ); // Async tool with external API call server.registerTool( 'fetch-weather', { title: 'Weather Fetcher', description: 'Get weather data for a city', inputSchema: { city: z.string() } }, async ({ city }) => { const response = await fetch(`https://api.weather.com/${city}`); const data = await response.text(); return { content: [{ type: 'text', text: data }] }; } ); // Tool that returns ResourceLinks server.registerTool( 'list-files', { title: 'List Files', description: 'List project files', inputSchema: { pattern: 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' } ] }) ); ``` #### 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. ### Prompts Prompts are reusable templates that help LLMs interact with your server effectively: ```typescript import { completable } from '@modelcontextprotocol/sdk/server/completable.js'; server.registerPrompt( 'review-code', { title: 'Code Review', description: 'Review code for best practices and potential issues', argsSchema: { code: z.string() } }, ({ code }) => ({ messages: [ { role: 'user', content: { type: 'text', text: `Please review this code:\n\n${code}` } } ] }) ); // Prompt with context-aware completion server.registerPrompt( 'team-greeting', { title: 'Team Greeting', description: 'Generate a greeting for team members', argsSchema: { department: completable(z.string(), value => { // Department suggestions return ['engineering', 'sales', 'marketing', 'support'].filter(d => d.startsWith(value)); }), name: completable(z.string(), (value, context) => { // Name suggestions based on selected department const department = context?.arguments?.['department']; if (department === 'engineering') { return ['Alice', 'Bob', 'Charlie'].filter(n => n.startsWith(value)); } else if (department === 'sales') { return ['David', 'Eve', 'Frank'].filter(n => n.startsWith(value)); } else if (department === 'marketing') { return ['Grace', 'Henry', 'Iris'].filter(n => n.startsWith(value)); } return ['Guest'].filter(n => n.startsWith(value)); }) } }, ({ department, name }) => ({ messages: [ { role: 'assistant', content: { type: 'text', text: `Hello ${name}, welcome to the ${department} team!` } } ] }) ); ``` ### Completions MCP supports argument completions to help users fill in prompt arguments and resource template parameters. See the examples above for [resource completions](#resources) and [prompt completions](#prompts). #### Client Usage ```typescript // Request completions for any argument const result = await client.complete({ ref: { type: 'ref/prompt', // or "ref/resource" name: 'example' // or uri: "template://..." }, argument: { name: 'argumentName', value: 'partial' // What the user has typed so far }, context: { // Optional: Include previously resolved arguments arguments: { previousArg: 'value' } } }); ``` ### 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. **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. #### Title Precedence for Tools For tools specifically, there are two ways to specify a title: - `title` field in the tool configuration - `annotations.title` field (when using the older `tool()` method with annotations) The precedence order is: `title` → `annotations.title` → `name` ```typescript // Using registerTool (recommended) server.registerTool( 'my_tool', { title: 'My Tool', // This title takes precedence annotations: { title: 'Annotation Title' // This is ignored if title is set } }, handler ); // Using tool with annotations (older API) server.tool( 'my_tool', 'description', { title: 'Annotation Title' // This is used as title }, handler ); ``` When building clients, use the provided utility to get the appropriate display name: ```typescript import { getDisplayName } from '@modelcontextprotocol/sdk/shared/metadataUtils.js'; // Automatically handles the precedence: title → annotations.title → name const displayName = getDisplayName(tool); ``` ### Sampling MCP servers can request LLM completions from connected clients that support sampling. ```typescript import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; const mcpServer = new McpServer({ name: 'tools-with-sample-server', version: '1.0.0' }); // Tool that uses LLM sampling to summarize any text mcpServer.registerTool( 'summarize', { description: 'Summarize any text using an LLM', inputSchema: { text: z.string().describe('Text to summarize') } }, async ({ text }) => { // Call the LLM through MCP sampling const response = await mcpServer.server.createMessage({ messages: [ { role: 'user', content: { type: 'text', text: `Please summarize the following text concisely:\n\n${text}` } } ], maxTokens: 500 }); return { content: [ { type: 'text', text: response.content.type === 'text' ? response.content.text : 'Unable to generate summary' } ] }; } ); async function main() { const transport = new StdioServerTransport(); await mcpServer.connect(transport); console.error('MCP server is running...'); } main().catch(error => { console.error('Server error:', error); process.exit(1); }); ``` ## Running Your Server 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 For command-line tools and direct integrations: ```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); ``` ### Streamable HTTP For remote servers, set up a Streamable HTTP transport that handles both client requests and server-to-client notifications. #### 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). ```typescript import express from 'express'; import { randomUUID } from 'node:crypto'; 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()); // Map to store transports by session ID const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; // Handle POST requests for client-to-server communication app.post('/mcp', async (req, res) => { // Check for existing session ID const sessionId = req.headers['mcp-session-id'] as string | undefined; let transport: StreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: sessionId => { // Store the transport by session ID transports[sessionId] = transport; } // DNS rebinding protection is disabled by default for backwards compatibility. If you are running this server // locally, make sure to set: // enableDnsRebindingProtection: true, // allowedHosts: ['127.0.0.1'], }); // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { delete transports[transport.sessionId]; } }; const server = new McpServer({ name: 'example-server', version: '1.0.0' }); // ... set up server resources, tools, and prompts ... // Connect to the MCP server await server.connect(transport); } else { // Invalid request res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, id: null }); return; } // Handle the request await transport.handleRequest(req, res, req.body); }); // Reusable handler for GET and DELETE requests const handleSessionRequest = async (req: express.Request, res: express.Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; if (!sessionId || !transports[sessionId]) { res.status(400).send('Invalid or missing session ID'); return; } const transport = transports[sessionId]; await transport.handleRequest(req, res); }; // Handle GET requests for server-to-client notifications via SSE app.get('/mcp', handleSessionRequest); // Handle DELETE requests for session termination 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: ```typescript import cors from 'cors'; // Add CORS middleware before your MCP routes app.use( cors({ origin: '*', // Configure appropriately for production, for example: // origin: ['https://your-remote-domain.com', 'https://your-other-remote-domain.com'], exposedHeaders: ['Mcp-Session-Id'], allowedHeaders: ['Content-Type', 'mcp-session-id'] }) ); ``` This configuration is necessary because: - The MCP streamable HTTP transport uses the `Mcp-Session-Id` header for session management - 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 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. try { const server = getServer(); const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); res.on('close', () => { console.log('Request closed'); transport.close(); server.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 }); } } }); // 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); 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. **Important**: If you are running this server locally, enable DNS rebinding protection: ```typescript const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), enableDnsRebindingProtection: true, allowedHosts: ['127.0.0.1', ...], allowedOrigins: ['https://yourdomain.com', 'https://www.yourdomain.com'] }); ``` ### Testing and Debugging To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information. ## Examples ### Echo Server A simple server demonstrating resources, tools, and prompts: ```typescript import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; const server = new McpServer({ name: 'echo-server', version: '1.0.0' }); server.registerResource( 'echo', new ResourceTemplate('echo://{message}', { list: undefined }), { title: 'Echo Resource', description: 'Echoes back messages as resources' }, async (uri, { message }) => ({ contents: [ { uri: uri.href, text: `Resource echo: ${message}` } ] }) ); 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', { title: 'Echo Prompt', description: 'Creates a prompt to process a message', argsSchema: { message: z.string() } }, ({ message }) => ({ messages: [ { role: 'user', content: { type: 'text', text: `Please process this message: ${message}` } } ] }) ); ``` ### SQLite Explorer A more complex example showing database integration: ```typescript import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import sqlite3 from 'sqlite3'; import { promisify } from 'util'; import { z } from 'zod'; const server = new McpServer({ name: 'sqlite-explorer', version: '1.0.0' }); // Helper to create DB connection const getDb = () => { const db = new sqlite3.Database('database.db'); return { all: promisify<string, any[]>(db.all.bind(db)), close: promisify(db.close.bind(db)) }; }; server.registerResource( 'schema', 'schema://main', { title: 'Database Schema', description: 'SQLite database schema', mimeType: 'text/plain' }, async uri => { const db = getDb(); try { const tables = await db.all("SELECT sql FROM sqlite_master WHERE type='table'"); return { contents: [ { uri: uri.href, text: tables.map((t: { sql: string }) => t.sql).join('\n') } ] }; } finally { await db.close(); } } ); server.registerTool( 'query', { title: 'SQL Query', description: 'Execute SQL queries on the database', inputSchema: { sql: z.string() } }, async ({ sql }) => { const db = getDb(); try { const results = await db.all(sql); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2) } ] }; } catch (err: unknown) { const error = err as Error; return { content: [ { type: 'text', text: `Error: ${error.message}` } ], isError: true }; } finally { await db.close(); } } ); ``` ## Advanced Usage ### Dynamic Servers 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 import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; const server = new McpServer({ name: 'Dynamic Example', version: '1.0.0' }); const listMessageTool = server.tool('listMessages', { channel: z.string() }, async ({ channel }) => ({ content: [{ type: 'text', text: await listMessages(channel) }] })); const putMessageTool = server.tool('putMessage', { channel: z.string(), message: z.string() }, async ({ channel, message }) => ({ content: [{ type: 'text', text: await putMessage(channel, message) }] })); // Until we upgrade auth, `putMessage` is disabled (won't show up in listTools) putMessageTool.disable(); const upgradeAuthTool = server.tool( 'upgradeAuth', { permission: z.enum(['write', 'admin']) }, // 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 we previously had read-only access, 'putMessage' is now available if (previous === 'read') { putMessageTool.enable(); } if (permission === 'write') { // 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 }); } else { // If we're now an admin, we no longer have anywhere to upgrade to, so fully remove that tool upgradeAuthTool.remove(); } } ); // Connect as normal const transport = new StdioServerTransport(); await server.connect(transport); ``` ### Improving Network Efficiency with Notification Debouncing When performing bulk updates that trigger notifications (e.g., enabling or disabling multiple tools in a loop), the SDK can send a large number of messages in a short period. To improve performance and reduce network traffic, you can enable notification debouncing. This feature coalesces multiple, rapid calls for the same notification type into a single message. For example, if you disable five tools in a row, only one `notifications/tools/list_changed` message will be sent instead of five. > [!IMPORTANT] This feature is designed for "simple" notifications that do not carry unique data in their parameters. To prevent silent data loss, debouncing is **automatically bypassed** for any notification that contains a `params` object or a `relatedRequestId`. Such > notifications will always be sent immediately. This is an opt-in feature configured during server initialization. ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; const server = new McpServer( { name: "efficient-server", version: "1.0.0" }, { // Enable notification debouncing for specific methods debouncedNotificationMethods: [ 'notifications/tools/list_changed', 'notifications/resources/list_changed', 'notifications/prompts/list_changed' ] } ); // Now, any rapid changes to tools, resources, or prompts will result // in a single, consolidated notification for each type. server.registerTool("tool1", ...).disable(); server.registerTool("tool2", ...).disable(); server.registerTool("tool3", ...).disable(); // Only one 'notifications/tools/list_changed' is sent. ``` ### Low-Level Server For more control, you can use the low-level Server class directly: ```typescript import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js'; const server = new Server( { name: 'example-server', version: '1.0.0' }, { capabilities: { prompts: {} } } ); server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [ { name: 'example-prompt', description: 'An example prompt template', arguments: [ { name: 'arg1', description: 'Example argument', required: true } ] } ] }; }); server.setRequestHandler(GetPromptRequestSchema, async request => { if (request.params.name !== 'example-prompt') { throw new Error('Unknown prompt'); } return { description: 'Example prompt', messages: [ { role: 'user', content: { type: 'text', text: 'Example prompt text' } } ] }; }); const transport = new StdioServerTransport(); await server.connect(transport); ``` ### Eliciting User Input MCP servers can request additional information from users through the elicitation feature. This is useful for interactive workflows where the server needs user input or confirmation: ```typescript // Server-side: Restaurant booking tool that asks for alternatives server.tool( 'book-restaurant', { restaurant: z.string(), date: z.string(), partySize: z.number() }, 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({ message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`, requestedSchema: { type: 'object', properties: { checkAlternatives: { type: 'boolean', title: 'Check alternative dates', description: 'Would you like me to check other dates?' }, flexibleDates: { type: 'string', title: 'Date flexibility', description: 'How flexible are your dates?', enum: ['next_day', 'same_week', 'next_week'], enumNames: ['Next day', 'Same week', 'Next week'] } }, required: ['checkAlternatives'] } }); if (result.action === 'accept' && result.content?.checkAlternatives) { const alternatives = await findAlternatives(restaurant, date, partySize, result.content.flexibleDates as string); return { content: [ { type: 'text', text: `Found these alternatives: ${alternatives.join(', ')}` } ] }; } return { content: [ { type: 'text', text: 'No booking made. Original date not available.' } ] }; } // Book the table await makeBooking(restaurant, date, partySize); return { content: [ { type: 'text', text: `Booked table for ${partySize} at ${restaurant} on ${date}` } ] }; } ); ``` Client-side: Handle elicitation requests ```typescript // This is a placeholder - implement based on your UI framework async function getInputFromUser( message: string, schema: any ): Promise<{ action: 'accept' | 'decline' | 'cancel'; data?: Record<string, any>; }> { // This should be implemented depending on the app throw new Error('getInputFromUser must be implemented for your platform'); } client.setRequestHandler(ElicitRequestSchema, async request => { const userResponse = await getInputFromUser(request.params.message, request.params.requestedSchema); return { action: userResponse.action, content: userResponse.action === 'accept' ? userResponse.data : undefined }; }); ``` **Note**: Elicitation requires client support. Clients must declare the `elicitation` capability during initialization. ### Writing MCP Clients The SDK provides a high-level client interface: ```typescript import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; const transport = new StdioClientTransport({ command: 'node', args: ['server.js'] }); const client = new Client({ name: 'example-client', version: '1.0.0' }); await client.connect(transport); // List prompts const prompts = await client.listPrompts(); // Get a prompt const prompt = await client.getPrompt({ name: 'example-prompt', arguments: { arg1: 'value' } }); // List resources const resources = await client.listResources(); // Read a resource const resource = await client.readResource({ uri: 'file:///example.txt' }); // Call a tool const result = await client.callTool({ name: 'example-tool', arguments: { arg1: 'value' } }); ``` ### Proxy Authorization Requests Upstream You can proxy OAuth requests to an external authorization provider: ```typescript import express from 'express'; import { ProxyOAuthServerProvider } from '@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js'; import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js'; const app = express(); const proxyProvider = new ProxyOAuthServerProvider({ endpoints: { authorizationUrl: 'https://auth.external.com/oauth2/v1/authorize', tokenUrl: 'https://auth.external.com/oauth2/v1/token', revocationUrl: 'https://auth.external.com/oauth2/v1/revoke' }, verifyAccessToken: async token => { return { token, clientId: '123', scopes: ['openid', 'email', 'profile'] }; }, getClient: async client_id => { return { client_id, redirect_uris: ['http://localhost:3000/callback'] }; } }); app.use( mcpAuthRouter({ provider: proxyProvider, issuerUrl: new URL('http://auth.external.com'), baseUrl: new URL('http://mcp.example.com'), serviceDocumentationUrl: new URL('https://docs.example.com/') }) ); ``` This setup allows you to: - Forward OAuth requests to an external provider - Add custom token validation logic - Manage client registrations - Provide custom documentation URLs - Maintain control over the OAuth flow while delegating to an external provider ### Backwards Compatibility Clients and servers with StreamableHttp transport can maintain [backwards compatibility](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#backwards-compatibility) with the deprecated HTTP+SSE transport (from protocol version 2024-11-05) as follows #### Client-Side Compatibility For clients that need to work with both Streamable HTTP and older SSE servers: ```typescript import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; let client: Client | undefined = undefined; const baseUrl = new URL(url); try { client = new Client({ name: 'streamable-http-client', version: '1.0.0' }); const transport = new StreamableHTTPClientTransport(new URL(baseUrl)); await client.connect(transport); console.log('Connected using Streamable HTTP transport'); } catch (error) { // If that fails with a 4xx error, try the older SSE transport console.log('Streamable HTTP connection failed, falling back to SSE transport'); client = new Client({ name: 'sse-client', version: '1.0.0' }); const sseTransport = new SSEClientTransport(baseUrl); await client.connect(sseTransport); console.log('Connected using SSE transport'); } ``` #### Server-Side Compatibility For servers that need to support both Streamable HTTP and older clients: ```typescript import express from 'express'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; const server = new McpServer({ name: 'backwards-compatible-server', version: '1.0.0' }); // ... set up server resources, tools, and prompts ... const app = express(); app.use(express.json()); // Store transports for each session type const transports = { streamable: {} as Record<string, StreamableHTTPServerTransport>, sse: {} as Record<string, SSEServerTransport> }; // Modern Streamable HTTP endpoint app.all('/mcp', async (req, res) => { // Handle Streamable HTTP transport for modern clients // Implementation as shown in the "With Session Management" example // ... }); // Legacy SSE endpoint for older clients app.get('/sse', async (req, res) => { // Create SSE transport for legacy clients const transport = new SSEServerTransport('/messages', res); transports.sse[transport.sessionId] = transport; res.on('close', () => { delete transports.sse[transport.sessionId]; }); await server.connect(transport); }); // Legacy message endpoint for older clients app.post('/messages', async (req, res) => { const sessionId = req.query.sessionId as string; const transport = transports.sse[sessionId]; if (transport) { await transport.handlePostMessage(req, res, req.body); } else { res.status(400).send('No transport found for sessionId'); } }); app.listen(3000); ``` **Note**: The SSE transport is now deprecated in favor of Streamable HTTP. New implementations should use Streamable HTTP, and existing SSE implementations should plan to migrate. ## Documentation - [Model Context Protocol documentation](https://modelcontextprotocol.io) - [MCP Specification](https://spec.modelcontextprotocol.io) - [Example Servers](https://github.com/modelcontextprotocol/servers) ## Contributing Issues and pull requests are welcome on GitHub at <https://github.com/modelcontextprotocol/typescript-sdk>. ## License This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.