- Overview
- Installation
- Quickstart
- What is MCP?
- Core Concepts
- Running Your Server
- Examples
- Advanced Usage
- Documentation
- Contributing
- License
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
npm install @modelcontextprotocol/sdk
⚠️ MCP requires Node.js v18.x or higher to work fine.
Let's create a simple MCP server that exposes a calculator tool and some data:
import{McpServer,ResourceTemplate}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{z}from"zod";// Create an MCP serverconstserver=newMcpServer({name: "demo-server",version: "1.0.0"});// Add an addition toolserver.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 resourceserver.registerResource("greeting",newResourceTemplate("greeting://{name}",{list: undefined}),{title: "Greeting Resource",// Display name for UIdescription: "Dynamic greeting generator"},async(uri,{ name })=>({contents: [{uri: uri.href,text: `Hello, ${name}!`}]}));// Start receiving messages on stdin and sending messages on stdoutconsttransport=newStdioServerTransport();awaitserver.connect(transport);The Model Context Protocol (MCP) 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!
The McpServer is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:
constserver=newMcpServer({name: "my-app",version: "1.0.0"});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:
// Static resourceserver.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 parametersserver.registerResource("user-profile",newResourceTemplate("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 completionserver.registerResource("repository",newResourceTemplate("github://repos/{owner}/{repo}",{list: undefined,complete: {// Provide intelligent completions based on previously resolved parametersrepo: (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 let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:
// Simple tool with parametersserver.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 callserver.registerTool("fetch-weather",{title: "Weather Fetcher",description: "Get weather data for a city",inputSchema: {city: z.string()}},async({ city })=>{constresponse=awaitfetch(`https://api.weather.com/${city}`);constdata=awaitresponse.text();return{content: [{type: "text",text: data}]};});// Tool that returns ResourceLinksserver.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'}]}));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 are reusable templates that help LLMs interact with your server effectively:
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 completionserver.registerPrompt("team-greeting",{title: "Team Greeting",description: "Generate a greeting for team members",argsSchema: {department: completable(z.string(),(value)=>{// Department suggestionsreturn["engineering","sales","marketing","support"].filter(d=>d.startsWith(value));}),name: completable(z.string(),(value,context)=>{// Name suggestions based on selected departmentconstdepartment=context?.arguments?.["department"];if(department==="engineering"){return["Alice","Bob","Charlie"].filter(n=>n.startsWith(value));}elseif(department==="sales"){return["David","Eve","Frank"].filter(n=>n.startsWith(value));}elseif(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!`}}]}));MCP supports argument completions to help users fill in prompt arguments and resource template parameters. See the examples above for resource completions and prompt completions.
// Request completions for any argumentconstresult=awaitclient.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 argumentsarguments: {previousArg: "value"}}});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.
For tools specifically, there are two ways to specify a title:
titlefield in the tool configurationannotations.titlefield (when using the oldertool()method with annotations)
The precedence order is: title → annotations.title → name
// Using registerTool (recommended)server.registerTool("my_tool",{title: "My Tool",// This title takes precedenceannotations: {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:
import{getDisplayName}from"@modelcontextprotocol/sdk/shared/metadataUtils.js";// Automatically handles the precedence: title → annotations.title → nameconstdisplayName=getDisplayName(tool);MCP servers can request LLM completions from connected clients that support sampling.
import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{z}from"zod";constmcpServer=newMcpServer({name: "tools-with-sample-server",version: "1.0.0",});// Tool that uses LLM sampling to summarize any textmcpServer.registerTool("summarize",{description: "Summarize any text using an LLM",inputSchema: {text: z.string().describe("Text to summarize"),},},async({ text })=>{// Call the LLM through MCP samplingconstresponse=awaitmcpServer.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",},],};});asyncfunctionmain(){consttransport=newStdioServerTransport();awaitmcpServer.connect(transport);console.log("MCP server is running...");}main().catch((error)=>{console.error("Server error:",error);process.exit(1);});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:
For command-line tools and direct integrations:
import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";constserver=newMcpServer({name: "example-server",version: "1.0.0"});// ... set up server resources, tools, and prompts ...consttransport=newStdioServerTransport();awaitserver.connect(transport);For remote servers, set up a Streamable HTTP transport that handles both client requests and server-to-client notifications.
In some cases, servers need to be stateful. This is achieved by session management.
importexpressfrom"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"constapp=express();app.use(express.json());// Map to store transports by session IDconsttransports: {[sessionId: string]: StreamableHTTPServerTransport}={};// Handle POST requests for client-to-server communicationapp.post('/mcp',async(req,res)=>{// Check for existing session IDconstsessionId=req.headers['mcp-session-id']asstring|undefined;lettransport: StreamableHTTPServerTransport;if(sessionId&&transports[sessionId]){// Reuse existing transporttransport=transports[sessionId];}elseif(!sessionId&&isInitializeRequest(req.body)){// New initialization requesttransport=newStreamableHTTPServerTransport({sessionIdGenerator: ()=>randomUUID(),onsessioninitialized: (sessionId)=>{// Store the transport by session IDtransports[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 closedtransport.onclose=()=>{if(transport.sessionId){deletetransports[transport.sessionId];}};constserver=newMcpServer({name: "example-server",version: "1.0.0"});// ... set up server resources, tools, and prompts ...// Connect to the MCP serverawaitserver.connect(transport);}else{// Invalid requestres.status(400).json({jsonrpc: '2.0',error: {code: -32000,message: 'Bad Request: No valid session ID provided',},id: null,});return;}// Handle the requestawaittransport.handleRequest(req,res,req.body);});// Reusable handler for GET and DELETE requestsconsthandleSessionRequest=async(req: express.Request,res: express.Response)=>{constsessionId=req.headers['mcp-session-id']asstring|undefined;if(!sessionId||!transports[sessionId]){res.status(400).send('Invalid or missing session ID');return;}consttransport=transports[sessionId];awaittransport.handleRequest(req,res);};// Handle GET requests for server-to-client notifications via SSEapp.get('/mcp',handleSessionRequest);// Handle DELETE requests for session terminationapp.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.
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:
importcorsfrom'cors';// Add CORS middleware before your MCP routesapp.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-Idheader 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
For simpler use cases where session management isn't needed:
constapp=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{constserver=getServer();consttransport: StreamableHTTPServerTransport=newStreamableHTTPServerTransport({sessionIdGenerator: undefined,});res.on('close',()=>{console.log('Request closed');transport.close();server.close();});awaitserver.connect(transport);awaittransport.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 modeapp.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 modeapp.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 serverconstPORT=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
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:
consttransport=newStreamableHTTPServerTransport({sessionIdGenerator: ()=>randomUUID(),enableDnsRebindingProtection: true,allowedHosts: ['127.0.0.1', ...],allowedOrigins: ['https://yourdomain.com','https://www.yourdomain.com']});To test your server, you can use the MCP Inspector. See its README for more information.
A simple server demonstrating resources, tools, and prompts:
import{McpServer,ResourceTemplate}from"@modelcontextprotocol/sdk/server/mcp.js";import{z}from"zod";constserver=newMcpServer({name: "echo-server",version: "1.0.0"});server.registerResource("echo",newResourceTemplate("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}`}}]}));A more complex example showing database integration:
import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";importsqlite3from"sqlite3";import{promisify}from"util";import{z}from"zod";constserver=newMcpServer({name: "sqlite-explorer",version: "1.0.0"});// Helper to create DB connectionconstgetDb=()=>{constdb=newsqlite3.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)=>{constdb=getDb();try{consttables=awaitdb.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{awaitdb.close();}});server.registerTool("query",{title: "SQL Query",description: "Execute SQL queries on the database",inputSchema: {sql: z.string()}},async({ sql })=>{constdb=getDb();try{constresults=awaitdb.all(sql);return{content: [{type: "text",text: JSON.stringify(results,null,2)}]};}catch(err: unknown){consterror=errasError;return{content: [{type: "text",text: `Error: ${error.message}`}],isError: true};}finally{awaitdb.close();}});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:
import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{z}from"zod";constserver=newMcpServer({name: "Dynamic Example",version: "1.0.0"});constlistMessageTool=server.tool("listMessages",{channel: z.string()},async({ channel })=>({content: [{type: "text",text: awaitlistMessages(channel)}]}));constputMessageTool=server.tool("putMessage",{channel: z.string(),message: z.string()},async({ channel, message })=>({content: [{type: "text",text: awaitputMessage(channel,message)}]}));// Until we upgrade auth, `putMessage` is disabled (won't show up in listTools)putMessageTool.disable()constupgradeAuthTool=server.tool("upgradeAuth",{permission: z.enum(["write","admin"])},// Any mutations here will automatically emit `listChanged` notificationsasync({ permission })=>{const{ ok, err, previous }=awaitupgradeAuthAndStoreToken(permission)if(!ok)return{content: [{type: "text",text: `Error: ${err}`}]}// If we previously had read-only access, 'putMessage' is now availableif(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 toolupgradeAuthTool.remove()}})// Connect as normalconsttransport=newStdioServerTransport();awaitserver.connect(transport);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.
import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";constserver=newMcpServer({name: "efficient-server",version: "1.0.0"},{// Enable notification debouncing for specific methodsdebouncedNotificationMethods: ['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.For more control, you can use the low-level Server class directly:
import{Server}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{ListPromptsRequestSchema,GetPromptRequestSchema}from"@modelcontextprotocol/sdk/types.js";constserver=newServer({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"){thrownewError("Unknown prompt");}return{description: "Example prompt",messages: [{role: "user",content: {type: "text",text: "Example prompt text"}}]};});consttransport=newStdioServerTransport();awaitserver.connect(transport);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:
// Server-side: Restaurant booking tool that asks for alternativesserver.tool("book-restaurant",{restaurant: z.string(),date: z.string(),partySize: z.number()},async({ restaurant, date, partySize })=>{// Check availabilityconstavailable=awaitcheckAvailability(restaurant,date,partySize);if(!available){// Ask user if they want to try alternative datesconstresult=awaitserver.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){constalternatives=awaitfindAlternatives(restaurant,date,partySize,result.content.flexibleDatesasstring);return{content: [{type: "text",text: `Found these alternatives: ${alternatives.join(", ")}`}]};}return{content: [{type: "text",text: "No booking made. Original date not available."}]};}// Book the tableawaitmakeBooking(restaurant,date,partySize);return{content: [{type: "text",text: `Booked table for ${partySize} at ${restaurant} on ${date}`}]};});Client-side: Handle elicitation requests
// This is a placeholder - implement based on your UI frameworkasyncfunctiongetInputFromUser(message: string,schema: any): Promise<{action: "accept"|"decline"|"cancel";data?: Record<string,any>;}>{// This should be implemented depending on the appthrownewError("getInputFromUser must be implemented for your platform");}client.setRequestHandler(ElicitRequestSchema,async(request)=>{constuserResponse=awaitgetInputFromUser(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.
The SDK provides a high-level client interface:
import{Client}from"@modelcontextprotocol/sdk/client/index.js";import{StdioClientTransport}from"@modelcontextprotocol/sdk/client/stdio.js";consttransport=newStdioClientTransport({command: "node",args: ["server.js"]});constclient=newClient({name: "example-client",version: "1.0.0"});awaitclient.connect(transport);// List promptsconstprompts=awaitclient.listPrompts();// Get a promptconstprompt=awaitclient.getPrompt({name: "example-prompt",arguments: {arg1: "value"}});// List resourcesconstresources=awaitclient.listResources();// Read a resourceconstresource=awaitclient.readResource({uri: "file:///example.txt"});// Call a toolconstresult=awaitclient.callTool({name: "example-tool",arguments: {arg1: "value"}});You can proxy OAuth requests to an external authorization provider:
importexpressfrom'express';import{ProxyOAuthServerProvider}from'@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js';import{mcpAuthRouter}from'@modelcontextprotocol/sdk/server/auth/router.js';constapp=express();constproxyProvider=newProxyOAuthServerProvider({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: newURL("http://auth.external.com"),baseUrl: newURL("http://mcp.example.com"),serviceDocumentationUrl: newURL("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
Clients and servers with StreamableHttp transport can maintain backwards compatibility with the deprecated HTTP+SSE transport (from protocol version 2024-11-05) as follows
For clients that need to work with both Streamable HTTP and older SSE servers:
import{Client}from"@modelcontextprotocol/sdk/client/index.js";import{StreamableHTTPClientTransport}from"@modelcontextprotocol/sdk/client/streamableHttp.js";import{SSEClientTransport}from"@modelcontextprotocol/sdk/client/sse.js";letclient: Client|undefined=undefinedconstbaseUrl=newURL(url);try{client=newClient({name: 'streamable-http-client',version: '1.0.0'});consttransport=newStreamableHTTPClientTransport(newURL(baseUrl));awaitclient.connect(transport);console.log("Connected using Streamable HTTP transport");}catch(error){// If that fails with a 4xx error, try the older SSE transportconsole.log("Streamable HTTP connection failed, falling back to SSE transport");client=newClient({name: 'sse-client',version: '1.0.0'});constsseTransport=newSSEClientTransport(baseUrl);awaitclient.connect(sseTransport);console.log("Connected using SSE transport");}For servers that need to support both Streamable HTTP and older clients:
importexpressfrom"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";constserver=newMcpServer({name: "backwards-compatible-server",version: "1.0.0"});// ... set up server resources, tools, and prompts ...constapp=express();app.use(express.json());// Store transports for each session typeconsttransports={streamable: {}asRecord<string,StreamableHTTPServerTransport>,sse: {}asRecord<string,SSEServerTransport>};// Modern Streamable HTTP endpointapp.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 clientsapp.get('/sse',async(req,res)=>{// Create SSE transport for legacy clientsconsttransport=newSSEServerTransport('/messages',res);transports.sse[transport.sessionId]=transport;res.on("close",()=>{deletetransports.sse[transport.sessionId];});awaitserver.connect(transport);});// Legacy message endpoint for older clientsapp.post('/messages',async(req,res)=>{constsessionId=req.query.sessionIdasstring;consttransport=transports.sse[sessionId];if(transport){awaittransport.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.
Issues and pull requests are welcome on GitHub at https://github.com/modelcontextprotocol/typescript-sdk.
This project is licensed under the MIT License—see the LICENSE file for details.