Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

The official TypeScript SDK for Model Context Protocol servers and clients

License

NotificationsYou must be signed in to change notification settings

modelcontextprotocol/typescript-sdk

Table of Contents

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 implementsthe full MCP specification, making it easy to:

  • 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

Installation

npm install @modelcontextprotocol/sdk zod

This SDK has arequired peer dependency onzod for schema validation. The SDK internally imports fromzod/v4, but maintains backwards compatibility with projects using Zod v3.25 or later. You can use either API in your code by importing fromzod/v3 orzod/v4:

Quick Start

Let's create a simple MCP server that exposes a calculator tool and some data. Save the following asserver.ts:

import{McpServer,ResourceTemplate}from'@modelcontextprotocol/sdk/server/mcp.js';import{StreamableHTTPServerTransport}from'@modelcontextprotocol/sdk/server/streamableHttp.js';importexpressfrom'express';import*aszfrom'zod/v4';// 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()},outputSchema:{result:z.number()}},async({ a, b})=>{constoutput={result:a+b};return{content:[{type:'text',text:JSON.stringify(output)}],structuredContent:output};});// 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}!`}]}));// Set up Express and HTTP transportconstapp=express();app.use(express.json());app.post('/mcp',async(req,res)=>{// Create a new transport for each request to prevent request ID collisionsconsttransport=newStreamableHTTPServerTransport({sessionIdGenerator:undefined,enableJsonResponse:true});res.on('close',()=>{transport.close();});awaitserver.connect(transport);awaittransport.handleRequest(req,res,req.body);});constport=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);});

Install the deps withnpm install @modelcontextprotocol/sdk express zod, and run withnpx -y tsx server.ts.

You can connect to it using any MCP client that supports streamable http, such as:

  • MCP Inspector:npx @modelcontextprotocol/inspector and connect to the streamable HTTP URLhttp://localhost:3000/mcp
  • Claude Code:claude mcp add --transport http my-server http://localhost:3000/mcp
  • VS Code:code --add-mcp "{\"name\":\"my-server\",\"type\":\"http\",\"url\":\"http://localhost:3000/mcp\"}"
  • Cursor: Click this deeplink

Then try asking your agent to add two numbers using its new tool!

Core Concepts

Server

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'});

Tools

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.

// Simple tool with parametersserver.registerTool('calculate-bmi',{title:'BMI Calculator',description:'Calculate Body Mass Index',inputSchema:{weightKg:z.number(),heightM:z.number()},outputSchema:{bmi:z.number()}},async({ weightKg, heightM})=>{constoutput={bmi:weightKg/(heightM*heightM)};return{content:[{type:'text',text:JSON.stringify(output)}],structuredContent:output};});// Async tool with external API callserver.registerTool('fetch-weather',{title:'Weather Fetcher',description:'Get weather data for a city',inputSchema:{city:z.string()},outputSchema:{temperature:z.number(),conditions:z.string()}},async({ city})=>{constresponse=awaitfetch(`https://api.weather.com/${city}`);constdata=awaitresponse.json();constoutput={temperature:data.temp,conditions:data.conditions};return{content:[{type:'text',text:JSON.stringify(output)}],structuredContent:output};});// Tool that returns ResourceLinksserver.registerTool('list-files',{title:'List Files',description:'List project files',inputSchema:{pattern:z.string()},outputSchema:{count:z.number(),files:z.array(z.object({name:z.string(),uri:z.string()}))}},async({ pattern})=>{constoutput={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};});

ResourceLinks

Tools can returnResourceLink 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 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.

// Static resourceserver.registerResource('config','config://app',{title:'Application Config',description:'Application configuration data',mimeType:'text/plain'},asyncuri=>({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}`}]}));

Prompts

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.

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!`}}]}));

Completions

MCP supports argument completions to help users fill in prompt arguments and resource template parameters. See the examples above forresource completions andprompt completions.

Client Usage

// 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'}}});

Display Names and Metadata

All resources, tools, and prompts support an optionaltitle field for better UI presentation. Thetitle is used as a display name (e.g. 'Create a new issue'), whilename remains the unique identifier (e.g.create_issue).

Note: Theregister* 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 oldertool() method with annotations)

The precedence order is:titleannotations.titlename

// 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);

Sampling

MCP servers can request LLM completions from connected clients that support sampling.

import{McpServer}from'@modelcontextprotocol/sdk/server/mcp.js';import{StreamableHTTPServerTransport}from'@modelcontextprotocol/sdk/server/streamableHttp.js';importexpressfrom'express';import*aszfrom'zod/v4';constmcpServer=newMcpServer({name:'tools-with-sample-server',version:'1.0.0'});// Tool that uses LLM sampling to summarize any textmcpServer.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 samplingconstresponse=awaitmcpServer.server.createMessage({messages:[{role:'user',content:{type:'text',text:`Please summarize the following text concisely:\n\n${text}`}}],maxTokens:500});constsummary=response.content.type==='text' ?response.content.text :'Unable to generate summary';constoutput={ summary};return{content:[{type:'text',text:JSON.stringify(output)}],structuredContent:output};});constapp=express();app.use(express.json());app.post('/mcp',async(req,res)=>{consttransport=newStreamableHTTPServerTransport({sessionIdGenerator:undefined,enableJsonResponse:true});res.on('close',()=>{transport.close();});awaitmcpServer.connect(transport);awaittransport.handleRequest(req,res,req.body);});constport=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);});

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:

Streamable HTTP

For remote servers, use the Streamable HTTP transport.

Without Session Management (Recommended)

For most use cases where session management isn't needed:

import{McpServer}from'@modelcontextprotocol/sdk/server/mcp.js';import{StreamableHTTPServerTransport}from'@modelcontextprotocol/sdk/server/streamableHttp.js';importexpressfrom'express';import*aszfrom'zod/v4';constapp=express();app.use(express.json());// Create the MCP server once (can be reused across requests)constserver=newMcpServer({name:'example-server',version:'1.0.0'});// Set up your tools, resources, and promptsserver.registerTool('echo',{title:'Echo Tool',description:'Echoes back the provided message',inputSchema:{message:z.string()},outputSchema:{echo:z.string()}},async({ message})=>{constoutput={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{consttransport=newStreamableHTTPServerTransport({sessionIdGenerator:undefined,enableJsonResponse:true});res.on('close',()=>{transport.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});}}});// Handle GET requests when session management is not supported - the server must return an HTTP 405 status code in this caseapp.get('/mcp',(req,res)=>{res.status(405).end();});constport=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 stateful sessions. This can be achieved bysession management in the MCP protocol.

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);

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. TheMcp-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 theMcp-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

DNS Rebinding Protection

The Streamable HTTP transport includes DNS rebinding protection to prevent security vulnerabilities. By default, this protection isdisabled 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']});

stdio

For local integrations spawned by another process, you can use the stdio transport:

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);

Testing and Debugging

To test your server, you can use theMCP Inspector. See its README for more information.

Node.js Web Crypto (globalThis.crypto) compatibility

Some parts of the SDK (for example, JWT-based client authentication inauth-extensions.ts viajose) rely on the Web Crypto API exposed asglobalThis.crypto.

  • Node.js v19.0.0 and later:globalThis.crypto is available by default.
  • Node.js v18.x:globalThis.crypto may not be defined by default; in this repository we polyfill it for tests (seevitest.setup.ts), and you should do the same in your app if it is missing - or alternatively, run Node with--experimental-global-webcrypto as per yourNode version documentation. (Seehttps://nodejs.org/dist/latest-v18.x/docs/api/globals.html#crypto )

If you run tests or applications on Node.js versions whereglobalThis.crypto is missing, you can polyfill it using the built-innode:crypto module, similar to the SDK's ownvitest.setup.ts:

import{webcrypto}from'node:crypto';if(typeofglobalThis.crypto==='undefined'){// eslint-disable-next-line @typescript-eslint/no-explicit-any(globalThisasany).crypto=webcryptoasunknownasCrypto;}

For production use, you can either:

  • Run on a Node.js version whereglobalThis.crypto is available by default (recommended), or
  • Apply a similar polyfill early in your application's startup code when targeting older Node.js runtimes.

Examples

Echo Server

A simple server demonstrating resources, tools, and prompts:

import{McpServer,ResourceTemplate}from'@modelcontextprotocol/sdk/server/mcp.js';import*aszfrom'zod/v4';constserver=newMcpServer({name:'echo-server',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})=>{constoutput={echo:`Tool echo:${message}`};return{content:[{type:'text',text:JSON.stringify(output)}],structuredContent:output};});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.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:

import{McpServer}from'@modelcontextprotocol/sdk/server/mcp.js';importsqlite3from'sqlite3';import{promisify}from'util';import*aszfrom'zod/v4';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'},asyncuri=>{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()},outputSchema:{rows:z.array(z.record(z.any())),rowCount:z.number()}},async({ sql})=>{constdb=getDb();try{constresults=awaitdb.all(sql);constoutput={rows:results,rowCount:results.length};return{content:[{type:'text',text:JSON.stringify(output,null,2)}],structuredContent:output};}catch(err:unknown){consterror=errasError;return{content:[{type:'text',text:`Error:${error.message}`}],isError:true};}finally{awaitdb.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 themafter the Server is connected. This will automatically emit the correspondinglistChanged notifications:

import{McpServer}from'@modelcontextprotocol/sdk/server/mcp.js';import{StreamableHTTPServerTransport}from'@modelcontextprotocol/sdk/server/streamableHttp.js';importexpressfrom'express';import*aszfrom'zod/v4';constserver=newMcpServer({name:'Dynamic Example',version:'1.0.0'});constlistMessageTool=server.registerTool('listMessages',{title:'List Messages',description:'List messages in a channel',inputSchema:{channel:z.string()},outputSchema:{messages:z.array(z.string())}},async({ channel})=>{constmessages=awaitlistMessages(channel);constoutput={ messages};return{content:[{type:'text',text:JSON.stringify(output)}],structuredContent:output};});constputMessageTool=server.registerTool('putMessage',{title:'Put Message',description:'Send a message to a channel',inputSchema:{channel:z.string(),message:z.string()},outputSchema:{success:z.boolean()}},async({ channel, message})=>{awaitputMessage(channel,message);constoutput={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();constupgradeAuthTool=server.registerTool('upgradeAuth',{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` notificationsasync({ permission})=>{const{ ok, err, previous}=awaitupgradeAuthAndStoreToken(permission);if(!ok){return{content:[{type:'text',text:`Error:${err}`}],isError:true};}// 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();}constoutput={success:true,newPermission:permission};return{content:[{type:'text',text:JSON.stringify(output)}],structuredContent:output};});// Connect with HTTP transportconstapp=express();app.use(express.json());app.post('/mcp',async(req,res)=>{consttransport=newStreamableHTTPServerTransport({sessionIdGenerator:undefined,enableJsonResponse:true});res.on('close',()=>{transport.close();});awaitserver.connect(transport);awaittransport.handleRequest(req,res,req.body);});constport=parseInt(process.env.PORT||'3000');app.listen(port,()=>{console.log(`MCP Server running on http://localhost:${port}/mcp`);});

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 onenotifications/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 isautomatically bypassed for any notification that contains aparams object or arelatedRequestId. Suchnotifications 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.

Low-Level Server

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,asyncrequest=>{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);

Eliciting User Input

MCP servers can request non-sensitive information from users through the form elicitation capability. This is useful for interactive workflows where the server needs user input or confirmation:

// Server-side: Restaurant booking tool that asks for alternativesserver.registerTool('book-restaurant',{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 availabilityconstavailable=awaitcheckAvailability(restaurant,date,partySize);if(!available){// Ask user if they want to try alternative datesconstresult=awaitserver.server.elicitInput({mode:'form',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);constoutput={success:false, alternatives};return{content:[{type:'text',text:JSON.stringify(output)}],structuredContent:output};}constoutput={success:false};return{content:[{type:'text',text:JSON.stringify(output)}],structuredContent:output};}// Book the tableawaitmakeBooking(restaurant,date,partySize);constoutput={success:true,booking:{ restaurant, date, partySize}};return{content:[{type:'text',text:JSON.stringify(output)}],structuredContent:output};});

On the client side, handle form 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,asyncrequest=>{constuserResponse=awaitgetInputFromUser(request.params.message,request.params.requestedSchema);return{action:userResponse.action,content:userResponse.action==='accept' ?userResponse.data :undefined};});

When callingserver.elicitInput, prefer to explicitly setmode: 'form' for new code. Omitting the mode continues to work for backwards compatibility and defaults to form elicitation.

Elicitation is a client capability. Clients must declare theelicitation capability during initialization:

constclient=newClient({name:'example-client',version:'1.0.0'},{capabilities:{elicitation:{form:{}}}});

Note: Form elicitationmust only be used to gather non-sensitive information. For sensitive information such as API keys or secrets, use URL elicitation instead.

Eliciting URL Actions

MCP servers can prompt the user to perform a URL-based action through URL elicitation. This is useful for securely gathering sensitive information such as API keys or secrets, or for redirecting users to secure web-based flows.

// Server-side: Prompt the user to navigate to a URLconstresult=awaitserver.server.elicitInput({mode:'url',message:'Please enter your API key',elicitationId:'550e8400-e29b-41d4-a716-446655440000',url:'http://localhost:3000/api-key'});// Alternative, return an error from within a tool:thrownewUrlElicitationRequiredError([{mode:'url',message:'This tool requires a payment confirmation. Open the link to confirm payment!',url:`http://localhost:${MCP_PORT}/confirm-payment?session=${sessionId}&elicitation=${elicitationId}&cartId=${encodeURIComponent(cartId)}`,elicitationId:'550e8400-e29b-41d4-a716-446655440000'}]);

On the client side, handle URL elicitation requests:

client.setRequestHandler(ElicitRequestSchema,asyncrequest=>{if(request.params.mode!=='url'){thrownewMcpError(ErrorCode.InvalidParams,`Unsupported elicitation mode:${request.params.mode}`);}// At a minimum, implement a UI that:// - Display the full URL and server reason to prevent phishing// - Explicitly ask the user for consent, with clear decline/cancel options// - Open the URL in the system (not embedded) browser// Optionally, listen for a `nofifications/elicitation/complete` message from the server});

Elicitation is a client capability. Clients must declare theelicitation capability during initialization:

constclient=newClient({name:'example-client',version:'1.0.0'},{capabilities:{elicitation:{url:{}}}});

Task-Based Execution

⚠️ Experimental API: Task-based execution is an experimental feature and may change without notice. Access these APIs via the.experimental.tasks namespace.

Task-based execution enables "call-now, fetch-later" patterns for long-running operations. This is useful for tools that take significant time to complete, where clients may want to disconnect and check on progress or retrieve results later.

Common use cases include:

  • Long-running data processing or analysis
  • Code migration or refactoring operations
  • Complex computational tasks
  • Operations that require periodic status updates

Server-Side: Implementing Task Support

To enable task-based execution, configure your server with aTaskStore implementation. The SDK doesn't provide a built-in TaskStore—you'll need to implement one backed by your database of choice:

import{Server}from'@modelcontextprotocol/sdk/server/index.js';import{TaskStore}from'@modelcontextprotocol/sdk/experimental';import{CallToolRequestSchema,ListToolsRequestSchema}from'@modelcontextprotocol/sdk/types.js';// Implement TaskStore backed by your database (e.g., PostgreSQL, Redis, etc.)classMyTaskStoreimplementsTaskStore{asynccreateTask(taskParams,requestId,request,sessionId?):Promise<Task>{// Generate unique taskId and lastUpdatedAt/createdAt timestamps// Store task in your database, using the session ID as a proxy to restrict unauthorized access// Return final Task object}asyncgetTask(taskId):Promise<Task|null>{// Retrieve task from your database}asyncupdateTaskStatus(taskId,status,statusMessage?):Promise<void>{// Update task status in your database}asyncstoreTaskResult(taskId,result):Promise<void>{// Store task result in your database}asyncgetTaskResult(taskId):Promise<Result>{// Retrieve task result from your database}asynclistTasks(cursor?,sessionId?):Promise<{tasks:Task[];nextCursor?:string}>{// List tasks with pagination support}}consttaskStore=newMyTaskStore();constserver=newServer({name:'task-enabled-server',version:'1.0.0'},{capabilities:{tools:{},// Declare capabilitiestasks:{list:{},cancel:{},requests:{tools:{// Declares support for tasks on tools/callcall:{}}}}},        taskStore// Enable task support});// Register a tool that supports tasks using the experimental APIserver.experimental.tasks.registerToolTask('my-echo-tool',{title:'My Echo Tool',description:'A simple task-based echo tool.',inputSchema:{message:z.string().describe('Message to send')}},{asynccreateTask({ message},{ taskStore, taskRequestedTtl, requestId}){// Create the taskconsttask=awaittaskStore.createTask({ttl:taskRequestedTtl});// Simulate out-of-band work(async()=>{awaitnewPromise(resolve=>setTimeout(resolve,5000));awaittaskStore.storeTaskResult(task.taskId,'completed',{content:[{type:'text',text:message}]});})();// Return CreateTaskResult with the created taskreturn{ task};},asyncgetTask(_args,{ taskId, taskStore}){// Retrieve the taskreturnawaittaskStore.getTask(taskId);},asyncgetTaskResult(_args,{ taskId, taskStore}){// Retrieve the result of the taskconstresult=awaittaskStore.getTaskResult(taskId);returnresultasCallToolResult;}});

Note: Seesrc/examples/shared/inMemoryTaskStore.ts in the SDK source for a reference task store implementation suitable for development and testing.

Client-Side: Using Task-Based Execution

Clients useexperimental.tasks.callToolStream() to initiate task-augmented tool calls. The returnedAsyncGenerator abstracts automatic polling and status updates:

import{Client}from'@modelcontextprotocol/sdk/client/index.js';import{CallToolResultSchema}from'@modelcontextprotocol/sdk/types.js';constclient=newClient({name:'task-client',version:'1.0.0'});// ... connect to server ...// Call the tool with task metadata using the experimental streaming APIconststream=client.experimental.tasks.callToolStream({name:'my-echo-tool',arguments:{message:'Hello, world!'}},CallToolResultSchema);// Iterate the stream and handle stream eventslettaskId='';forawait(constmessageofstream){switch(message.type){case'taskCreated':console.log('Task created successfully with ID:',message.task.taskId);taskId=message.task.taskId;break;case'taskStatus':console.log(`${message.task.status}${message.task.statusMessage??''}`);break;case'result':console.log('Task completed! Tool result:');message.result.content.forEach(item=>{if(item.type==='text'){console.log(`${item.text}`);}});break;case'error':throwmessage.error;}}// Optional: Fire and forget - disconnect and reconnect later// (useful when you don't want to wait for long-running tasks)// Later, after disconnecting and reconnecting to the server:consttaskStatus=awaitclient.getTask({ taskId});console.log('Task status:',taskStatus.status);if(taskStatus.status==='completed'){consttaskResult=awaitclient.getTaskResult({ taskId},CallToolResultSchema);console.log('Retrieved result after reconnect:',taskResult);}

Theexperimental.tasks.callToolStream() method also works with non-task tools, making it a drop-in replacement forcallTool() in applications that support it. When used to invoke a tool that doesn't support tasks, thetaskCreated andtaskStatus events will not be emitted.

Task Status Lifecycle

Tasks transition through the following states:

  • working: Task is actively being processed
  • input_required: Task is waiting for additional input (e.g., from elicitation)
  • completed: Task finished successfully
  • failed: Task encountered an error
  • cancelled: Task was cancelled by the client

Thettl parameter suggests how long the server will manage the task for. If the task duration exceeds this, the server may delete the task prematurely. The client's suggested value may be overridden by the server, and the final TTL will be provided inTask.ttl intaskCreated andtaskStatus events.

Writing MCP Clients

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'}});

OAuth client authentication helpers

For OAuth-secured MCP servers, the clientauth module exposes a genericOAuthClientProvider interface, andsrc/client/auth-extensions.ts provides ready-to-use implementations for common machine-to-machine authentication flows:

  • ClientCredentialsProvider: Uses theclient_credentials grant withclient_secret_basic authentication.
  • PrivateKeyJwtProvider: Uses theclient_credentials grant withprivate_key_jwt client authentication, signing a JWT assertion on each token request.
  • StaticPrivateKeyJwtProvider: Similar toPrivateKeyJwtProvider, but accepts a pre-built JWT assertion string viajwtBearerAssertion and reuses it for token requests.

You can use these providers with theStreamableHTTPClientTransport and the high-levelauth() helper:

import{Client}from'@modelcontextprotocol/sdk/client/index.js';import{StreamableHTTPClientTransport}from'@modelcontextprotocol/sdk/client/streamableHttp.js';import{ClientCredentialsProvider,PrivateKeyJwtProvider,StaticPrivateKeyJwtProvider}from'@modelcontextprotocol/sdk/client/auth-extensions.js';import{auth}from'@modelcontextprotocol/sdk/client/auth.js';constserverUrl=newURL('https://mcp.example.com/');// Example: client_credentials with client_secret_basicconstbasicProvider=newClientCredentialsProvider({clientId:process.env.CLIENT_ID!,clientSecret:process.env.CLIENT_SECRET!,clientName:'example-basic-client'});// Example: client_credentials with private_key_jwt (JWT signed locally)constprivateKeyJwtProvider=newPrivateKeyJwtProvider({clientId:process.env.CLIENT_ID!,privateKey:process.env.CLIENT_PRIVATE_KEY_PEM!,algorithm:'RS256',clientName:'example-private-key-jwt-client',jwtLifetimeSeconds:300});// Example: client_credentials with a pre-built JWT assertionconststaticJwtProvider=newStaticPrivateKeyJwtProvider({clientId:process.env.CLIENT_ID!,jwtBearerAssertion:process.env.CLIENT_ASSERTION!,clientName:'example-static-private-key-jwt-client'});consttransport=newStreamableHTTPClientTransport(serverUrl,{authProvider:privateKeyJwtProvider});constclient=newClient({name:'example-client',version:'1.0.0'});// Perform the OAuth flow (including dynamic client registration if needed)awaitauth(privateKeyJwtProvider,{ serverUrl,fetchFn:transport.fetch});awaitclient.connect(transport);

If you need lower-level control, you can also usecreatePrivateKeyJwtAuth() directly to implementaddClientAuthentication on a customOAuthClientProvider.

Proxy Authorization Requests Upstream

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:asynctoken=>{return{            token,clientId:'123',scopes:['openid','email','profile']};},getClient:asyncclient_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

Backwards Compatibility

Clients and servers with StreamableHttp transport can maintainbackwards 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:

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=undefined;constbaseUrl=newURL(url);try{client=newClient({name:'streamable-http-client',version:'1.0.0'});consttransport=newStreamableHTTPClientTransport(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');}

Server-Side Compatibility

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.

Documentation

Contributing

Issues and pull requests are welcome on GitHub athttps://github.com/modelcontextprotocol/typescript-sdk.

License

This project is licensed under the MIT License—see theLICENSE file for details.

About

The official TypeScript SDK for Model Context Protocol servers and clients

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks


[8]ページ先頭

©2009-2025 Movatter.jp