Server
Server overview
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:
- Streamable HTTP (recommended for remote servers)
- HTTP + SSE (deprecated, for backwards compatibility only)
- stdio (for local, process‑spawned integrations)
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 clients
Transports
Streamable HTTP
Streamable HTTP is the modern, fully featured transport. It supports:
- Request/response over HTTP POST
- Server‑to‑client notifications over SSE (when enabled)
- Optional JSON‑only response mode with no SSE
- Session management and resumability
Key examples:
simpleStreamableHttp.ts– sessions, logging, tasks, elicitation, auth hooksjsonResponseStreamableHttp.ts–enableJsonResponse: true, no SSEstandaloneSseWithGetStreamableHttp.ts– notifications with Streamable HTTP GET + SSE
See the MCP spec for full transport details: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports
Stateless vs stateful sessions
Streamable HTTP can run:
- Stateless – no session tracking, ideal for simple API‑style servers.
- Stateful – sessions have IDs, and you can enable resumability and advanced features.
Examples:
- Stateless Streamable HTTP:
simpleStatelessStreamableHttp.ts - Stateful with resumability:
simpleStreamableHttp.ts
A stateful server keeps one transport per session in memory, so it should close sessions that have been idle for a while and limit how many can be open at once. simpleStreamableHttp.ts shows both; pick a limit that fits in memory.
stdio
For 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).
Deprecated HTTP + SSE
The older HTTP+SSE transport (protocol version 2024‑11‑05) is supported only for backwards compatibility. New implementations should prefer Streamable HTTP.
Examples:
- Legacy SSE server:
simpleSseServer.ts - Backwards‑compatible server (Streamable HTTP + SSE):
sseAndStreamableHttpCompatibleServer.ts
Running your server
For a minimal “getting started” experience:
- Start from
simpleStreamableHttp.ts. - Remove features you do not need (tasks, advanced logging, OAuth, etc.).
- Register your own tools, resources and prompts.
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.
DNS rebinding protection
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']));Bearer token authentication
requireBearerAuth verifies the Authorization: Bearer token of a request with a verifier you supply and puts the resulting AuthInfo on req.auth. To accept only tokens issued for this resource (the token's audience), set expectedResource to the value your authorization server puts into tokens meant for this server, usually the server's URL, and have the verifier report that value in AuthInfo.resource:
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
const mcpServerUrl = new URL('https://api.example.com/mcp');
const auth = requireBearerAuth({
verifier: {
async verifyAccessToken(token) {
const payload = await verifyJwt(token); // your JWT library or introspection call
// `aud` is one value, a list, or absent: report the entry on this server's origin, if any, and let `expectedResource` compare it.
const audience = [payload.aud ?? []].flat().find(aud => URL.canParse(aud) && new URL(aud).origin === mcpServerUrl.origin);
const resource = audience ? new URL(audience) : undefined;
return { token, clientId: payload.sub, scopes: payload.scopes, expiresAt: payload.exp, resource };
}
},
expectedResource: mcpServerUrl
});
app.post('/mcp', auth, handleMcpRequest);With expectedResource set, a token is accepted only if AuthInfo.resource is that value (one trailing slash aside); a token reported for another value, or for none, gets 401 invalid_token. Without it, AuthInfo.resource is not compared with anything. simpleStreamableHttp.ts sets it when started with --oauth --oauth-strict.
Tools, resources, and prompts
Tools
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:
Image and audio results
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'
}
]
})
);Embedded resource results
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' })
}
}
]
}));Error handling
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
};
}
});Tool change notifications
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();ResourceLink outputs
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.
Limit the number of elements in tool arguments
A tools/call request can carry very large arrays and objects in its arguments. McpServer can limit the number of elements it accepts. Pass maxToolInputElements to the constructor: the largest number of array elements and object members, combined, that a single call's arguments may contain. It must be a number of at least 1, or Infinity for no limit.
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { maxToolInputElements: 10_000 });It is off by default — leave it unset and behavior is unchanged. When it is set and a call exceeds it, that call comes back as an isError: true tool result naming the limit, before the schema runs, and the server keeps handling other calls. Choose a value above the largest arguments your tools legitimately accept. The transport's maxRequestBodySize (4 MiB by default) is the primary limit on request size; maxToolInputElements is a finer limit on the number of elements for servers that may receive large payloads.
Resources
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' }]
})
);Binary resources
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 }]
}));Resource templates
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))
}
]
}));Subscribing and unsubscribing
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
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}`
}
}
]
})
);Image content in prompts
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'
}
}
]
})
);Embedded resources in prompts
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...'
}
}
}
]
})
);Prompt change notifications
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:
Completions
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']Logging
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.
Log level filtering
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' }); // sentDisplay names and metadata
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:
getDisplayNamefrom@modelcontextprotocol/sdk/shared/metadataUtils.js
Multi‑node deployment patterns
The SDK supports multi‑node deployments using Streamable HTTP. The high‑level patterns are documented in README.md:
- Stateless mode (any node can handle any request)
- Persistent storage mode (shared database for session state)
- Local state with message routing (message queue + pub/sub)
Those deployment diagrams are kept in README.md so the examples and documentation stay aligned.
Backwards compatibility
To handle both modern and legacy clients:
- Run a backwards‑compatible server:
- Use a client that falls back from Streamable HTTP to SSE:
For the detailed protocol rules, see the “Backwards compatibility” section of the MCP spec.