This SDK lets you build MCP servers in TypeScript and connect them to different transports. For most use cases you will use McpServer from @modelcontextprotocol/sdk/server/mcp.js and choose one of:
For a complete, runnable example server, see:
simpleStreamableHttp.ts – feature‑rich Streamable HTTP serverjsonResponseStreamableHttp.ts – Streamable HTTP with JSON response modesimpleStatelessStreamableHttp.ts – stateless Streamable HTTP serversimpleSseServer.ts – deprecated HTTP+SSE transportsseAndStreamableHttpCompatibleServer.ts – backwards‑compatible server for old and new clientsStreamable HTTP is the modern, fully featured transport. It supports:
Key examples:
simpleStreamableHttp.ts – sessions, logging, tasks, elicitation, auth hooksjsonResponseStreamableHttp.ts – enableJsonResponse: true, no SSEstandaloneSseWithGetStreamableHttp.ts – notifications with Streamable HTTP GET + SSESee the MCP spec for full transport details: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports
Streamable HTTP can run:
Examples:
simpleStatelessStreamableHttp.tssimpleStreamableHttp.tsFor local integrations where the client spawns the server as a child process, use StdioServerTransport. Communication happens over stdin/stdout using JSON-RPC:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
// ... register tools, resources, prompts ...
const transport = new StdioServerTransport();
await server.connect(transport);
This is the simplest transport — no HTTP server setup required. The client uses StdioClientTransport to spawn and communicate with the server process (see docs/client.md).
The older HTTP+SSE transport (protocol version 2024‑11‑05) is supported only for backwards compatibility. New implementations should prefer Streamable HTTP.
Examples:
simpleSseServer.tssseAndStreamableHttpCompatibleServer.tsFor a minimal “getting started” experience:
simpleStreamableHttp.ts.For more detailed patterns (stateless vs stateful, JSON response mode, CORS, DNS rebind protection), see the examples above and the MCP spec sections on transports.
MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use createMcpExpressApp() to create an Express app with DNS rebinding protection enabled by default:
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
// Protection auto-enabled (default host is 127.0.0.1)
const app = createMcpExpressApp();
// Protection auto-enabled for localhost
const app = createMcpExpressApp({ host: 'localhost' });
// No auto protection when binding to all interfaces
const app = createMcpExpressApp({ host: '0.0.0.0' });
For custom host validation, use the middleware directly:
import express from 'express';
import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js';
const app = express();
app.use(express.json());
app.use(hostHeaderValidation(['localhost', '127.0.0.1', 'myhost.local']));
Tools let MCP clients ask your server to take actions. They are usually the main way that LLMs call into your application.
A typical registration with registerTool looks like this:
server.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 }) => {
const output = { bmi: weightKg / (heightM * heightM) };
return {
content: [{ type: 'text', text: JSON.stringify(output) }],
structuredContent: output
};
}
);
This snippet is illustrative only; for runnable servers that expose tools, see:
Tools can return image and audio content alongside text. Use base64-encoded data with the appropriate MIME type:
// e.g. const chartPngBase64 = fs.readFileSync('chart.png').toString('base64');
server.registerTool('generate-chart', { description: 'Generate a chart image' }, async () => ({
content: [
{
type: 'image',
data: chartPngBase64,
mimeType: 'image/png'
}
]
}));
// e.g. const audioBase64 = fs.readFileSync('speech.wav').toString('base64');
server.registerTool(
'text-to-speech',
{
description: 'Convert text to speech',
inputSchema: { text: z.string() }
},
async ({ text }) => ({
content: [
{
type: 'audio',
data: audioBase64,
mimeType: 'audio/wav'
}
]
})
);
Tools can return embedded resources, allowing the tool to attach full resource objects in its response:
server.registerTool('fetch-data', { description: 'Fetch and return data as a resource' }, async () => ({
content: [
{
type: 'resource',
resource: {
uri: 'data://result',
mimeType: 'application/json',
text: JSON.stringify({ key: 'value' })
}
}
]
}));
To indicate that a tool call failed, set isError: true in the result. The content describes what went wrong:
server.registerTool('risky-operation', { description: 'An operation that might fail' }, async () => {
try {
const result = await doSomething();
return { content: [{ type: 'text', text: result }] };
} catch (err) {
return {
content: [{ type: 'text', text: `Error: ${err.message}` }],
isError: true
};
}
});
When tools are added, removed, or updated at runtime, the server automatically notifies connected clients. This happens when you call registerTool(), or use remove(), enable(), disable(), or update() on a RegisteredTool. You can also trigger it manually:
server.sendToolListChanged();
Tools can return resource_link content items to reference large resources without embedding them directly, allowing clients to fetch only what they need.
The README's list-files example shows the pattern conceptually; for concrete usage, see the Streamable HTTP examples in src/examples/server.
Resources expose data to clients, but should not perform heavy computation or side‑effects. They are ideal for configuration, documents, or other reference data.
Conceptually, you might register resources like:
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' }]
})
);
Resources can return binary data using blob (base64-encoded) instead of text:
server.registerResource('logo', 'images://logo.png', { title: 'Logo', mimeType: 'image/png' }, async uri => ({
contents: [{ uri: uri.href, blob: logoPngBase64 }]
}));
Dynamic resources use ResourceTemplate to match URI patterns. The template parameters are passed to the read callback:
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
server.registerResource('user-profile', new ResourceTemplate('users://{userId}/profile', { list: undefined }), { title: 'User Profile', mimeType: 'application/json' }, async (uri, { userId }) => ({
contents: [
{
uri: uri.href,
text: JSON.stringify(await getUser(userId))
}
]
}));
Clients can subscribe to resource changes. The server declares subscription support via the resources.subscribe capability, which McpServer enables automatically when resources are registered.
To handle subscriptions, register handlers on the low-level server for SubscribeRequestSchema and UnsubscribeRequestSchema:
import { SubscribeRequestSchema, UnsubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';
const subscriptions = new Set<string>();
server.server.setRequestHandler(SubscribeRequestSchema, async request => {
subscriptions.add(request.params.uri);
return {};
});
server.server.setRequestHandler(UnsubscribeRequestSchema, async request => {
subscriptions.delete(request.params.uri);
return {};
});
When a subscribed resource changes, notify the client:
if (subscriptions.has(resourceUri)) {
await server.server.sendResourceUpdated({ uri: resourceUri });
}
Resource list changes (adding/removing resources) are notified automatically when using registerResource(), remove(), enable(), or disable(). You can also trigger it manually:
server.sendResourceListChanged();
For full runnable examples of resources:
Prompts are reusable templates that help humans (or client UIs) talk to models in a consistent way. They are declared on the server and listed through MCP.
A minimal prompt:
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}`
}
}
]
})
);
Prompts can include image content in their messages:
server.registerPrompt(
'analyze-image',
{
title: 'Analyze Image',
description: 'Analyze an image',
argsSchema: { imageBase64: z.string() }
},
({ imageBase64 }) => ({
messages: [
{
role: 'user',
content: {
type: 'image',
data: imageBase64,
mimeType: 'image/png'
}
}
]
})
);
Prompts can embed resource content in their messages:
server.registerPrompt(
'summarize-doc',
{
title: 'Summarize Document',
description: 'Summarize a document resource'
},
() => ({
messages: [
{
role: 'user',
content: {
type: 'resource',
resource: {
uri: 'docs://readme',
mimeType: 'text/plain',
text: 'Document content here...'
}
}
}
]
})
);
Like tools, prompt list changes are notified automatically when using registerPrompt(), remove(), enable(), or disable(). You can also trigger it manually:
server.sendPromptListChanged();
For prompts integrated into a full server, see:
Both prompts and resources can support argument completions using the completable wrapper. This lets clients offer autocomplete suggestions as users type.
import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
server.registerPrompt(
'greet',
{
title: 'Greeting',
description: 'Generate a greeting',
argsSchema: {
name: completable(z.string(), value => {
// Return suggestions matching the partial input
const names = ['Alice', 'Bob', 'Charlie'];
return names.filter(n => n.toLowerCase().startsWith(value.toLowerCase()));
})
}
},
({ name }) => ({
messages: [{ role: 'user', content: { type: 'text', text: `Hello, ${name}!` } }]
})
);
Resource templates also support completions on their path parameters via completable. On the client side, use client.complete() with a reference to the prompt or resource and the partially-typed argument:
const result = await client.complete({
ref: { type: 'ref/prompt', name: 'greet' },
argument: { name: 'name', value: 'Al' }
});
console.log(result.completion.values); // ['Alice']
The server can send log messages to the client using server.sendLoggingMessage(). Clients can request a minimum log level via the logging/setLevel request, which McpServer handles automatically — messages below the requested level are suppressed.
// Send a log message from a tool handler:
server.registerTool(
'process-data',
{
description: 'Process some data',
inputSchema: { data: z.string() }
},
async ({ data }, extra) => {
await server.sendLoggingMessage({ level: 'info', data: `Processing: ${data}` }, extra.sessionId);
// ... do work ...
return { content: [{ type: 'text', text: 'Done' }] };
}
);
For a full example, see simpleStreamableHttp.ts which uses sendLoggingMessage throughout.
Log levels in order: debug, info, notice, warning, error, critical, alert, emergency.
Clients can request a minimum log level via logging/setLevel. The low-level Server handles this automatically when the logging capability is enabled — it stores the requested level per session and suppresses messages below it. You can also send log messages directly using
sendLoggingMessage:
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { logging: {} } });
// Client requests: only show 'warning' and above
// (handled automatically by the Server)
// These will be sent or suppressed based on the client's requested level:
await server.sendLoggingMessage({ level: 'debug', data: 'verbose detail' }); // suppressed
await server.sendLoggingMessage({ level: 'warning', data: 'something is off' }); // sent
await server.sendLoggingMessage({ level: 'error', data: 'something broke' }); // sent
Tools, resources and prompts support a title field for human‑readable names. Older APIs can also attach annotations.title. To compute the correct display name on the client, use:
getDisplayName from @modelcontextprotocol/sdk/shared/metadataUtils.jsThe SDK supports multi‑node deployments using Streamable HTTP. The high‑level patterns are documented in README.md:
Those deployment diagrams are kept in README.md so the examples and documentation stay aligned.
To handle both modern and legacy clients:
For the detailed protocol rules, see the “Backwards compatibility” section of the MCP spec.