Skip to content

Latest commit

 

History

History
489 lines (391 loc) · 21.7 KB

File metadata and controls

489 lines (391 loc) · 21.7 KB
title
Server Guide

Server overview

This guide covers SDK usage for building MCP servers in TypeScript. For protocol-level details and message formats, see the MCP specification.

Building a server takes three steps:

  1. Create an {@linkcode @modelcontextprotocol/server!server/mcp.McpServer | McpServer} and register your tools, resources, and prompts.
  2. Create a transport — Streamable HTTP for remote servers or stdio for local, process‑spawned integrations.
  3. Wire the transport into your HTTP framework (or use stdio directly) and call server.connect(transport).

The sections below cover each of these. For a feature‑rich starting point, see simpleStreamableHttp.ts — remove what you don't need and register your own tools, resources, and prompts. For stateless or JSON‑response‑mode alternatives, see the examples linked in Transports below.

Transports

Streamable HTTP

Streamable HTTP is the HTTP‑based 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

A minimal stateless server using createMcpExpressApp(), which includes DNS rebinding protection by default:

const app = createMcpExpressApp();

app.post('/mcp', async (req, res) => {
    const server = new McpServer({ name: 'my-server', version: '1.0.0' });
    const transport = new NodeStreamableHTTPServerTransport({
        sessionIdGenerator: undefined // stateless
    });
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
});

app.listen(3000, '127.0.0.1');

For stateful servers with session management, see simpleStreamableHttp.ts.

Note

For full runnable examples, see simpleStreamableHttp.ts (sessions, logging, tasks, elicitation, auth hooks), jsonResponseStreamableHttp.ts (enableJsonResponse: true, no SSE), and standaloneSseWithGetStreamableHttp.ts (notifications with Streamable HTTP GET + SSE).

For protocol details, see Transports in the MCP specification.

Warning

If your server listens on localhost, use createMcpExpressApp() or createMcpHonoApp() instead of using NodeStreamableHTTPServerTransport directly — they include DNS rebinding protection by default.

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.

The key difference is the sessionIdGenerator option. Pass undefined for stateless mode:

const server = new McpServer({ name: 'my-server', version: '1.0.0' });

const transport = new NodeStreamableHTTPServerTransport({
    sessionIdGenerator: undefined
});

await server.connect(transport);

Note

For full runnable examples, see simpleStatelessStreamableHttp.ts (stateless) and simpleStreamableHttp.ts (stateful with resumability).

JSON response mode

If you do not need SSE streaming, set enableJsonResponse: true. The server will return plain JSON responses to every POST and reject GET requests with 405:

const server = new McpServer({ name: 'my-server', version: '1.0.0' });

const transport = new NodeStreamableHTTPServerTransport({
    sessionIdGenerator: () => randomUUID(),
    enableJsonResponse: true
});

await server.connect(transport);

Note

For a full runnable example, see jsonResponseStreamableHttp.ts.

stdio

For local, process‑spawned integrations (Claude Desktop, CLI tools), use {@linkcode @modelcontextprotocol/server!server/stdio.StdioServerTransport | StdioServerTransport}:

const server = new McpServer({ name: 'my-server', version: '1.0.0' });
const transport = new StdioServerTransport();
await server.connect(transport);

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 {@linkcode @modelcontextprotocol/server!server/mcp.McpServer#registerTool | registerTool}:

server.registerTool(
    'calculate-bmi',
    {
        title: 'BMI Calculator',
        description: 'Calculate Body Mass Index',
        inputSchema: z.object({
            weightKg: z.number(),
            heightM: z.number()
        }),
        outputSchema: z.object({ bmi: z.number() })
    },
    async ({ weightKg, heightM }) => {
        const output = { bmi: weightKg / (heightM * heightM) };
        return {
            content: [{ type: 'text', text: JSON.stringify(output) }],
            structuredContent: output
        };
    }
);

Note

For full runnable examples, see simpleStreamableHttp.ts and toolWithSampleServer.ts.

For protocol details, see Tools in the MCP specification.

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:

server.registerTool(
    'list-files',
    {
        title: 'List Files',
        description: 'Returns files as resource links without embedding content'
    },
    async (): Promise<CallToolResult> => {
        const links: ResourceLink[] = [
            {
                type: 'resource_link',
                uri: 'file:///projects/readme.md',
                name: 'README',
                mimeType: 'text/markdown'
            },
            {
                type: 'resource_link',
                uri: 'file:///projects/config.json',
                name: 'Config',
                mimeType: 'application/json'
            }
        ];
        return { content: links };
    }
);

Note

For a full runnable example with ResourceLink outputs, see simpleStreamableHttp.ts.

Tool annotations

Tools can include annotations that hint at their behavior — for example, whether a tool is read‑only, destructive, or idempotent. Annotations help clients present tools appropriately without changing their execution semantics.

Note

For tool annotations in a full server, see simpleStreamableHttp.ts.

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.

A static resource at a fixed URI:

server.registerResource(
    'config',
    'config://app',
    {
        title: 'Application Config',
        description: 'Application configuration data',
        mimeType: 'text/plain'
    },
    async uri => ({
        contents: [{ uri: uri.href, text: 'App configuration here' }]
    })
);

Dynamic resources use {@linkcode @modelcontextprotocol/server!server/mcp.ResourceTemplate | ResourceTemplate} and can support completions on path parameters:

server.registerResource(
    'user-profile',
    new ResourceTemplate('user://{userId}/profile', {
        list: async () => ({
            resources: [
                { uri: 'user://123/profile', name: 'Alice' },
                { uri: 'user://456/profile', name: 'Bob' }
            ]
        })
    }),
    {
        title: 'User Profile',
        description: 'User profile data',
        mimeType: 'application/json'
    },
    async (uri, { userId }) => ({
        contents: [
            {
                uri: uri.href,
                text: JSON.stringify({ userId, name: 'Example User' })
            }
        ]
    })
);

Note

For full runnable examples of resources, see simpleStreamableHttp.ts.

For protocol details, see Resources in the MCP specification.

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: z.object({
            code: z.string()
        })
    },
    ({ code }) => ({
        messages: [
            {
                role: 'user' as const,
                content: {
                    type: 'text' as const,
                    text: `Please review this code:\n\n${code}`
                }
            }
        ]
    })
);

Note

For prompts integrated into a full server, see simpleStreamableHttp.ts.

For protocol details, see Prompts in the MCP specification.

Completions

Both prompts and resources can support argument completions. Wrap a field in the argsSchema with {@linkcode @modelcontextprotocol/server!server/completable.completable | completable()} to provide autocompletion suggestions:

server.registerPrompt(
    'review-code',
    {
        title: 'Code Review',
        description: 'Review code for best practices',
        argsSchema: z.object({
            language: completable(z.string().describe('Programming language'), value =>
                ['typescript', 'javascript', 'python', 'rust', 'go'].filter(lang => lang.startsWith(value))
            )
        })
    },
    ({ language }) => ({
        messages: [
            {
                role: 'user' as const,
                content: {
                    type: 'text' as const,
                    text: `Review this ${language} code for best practices.`
                }
            }
        ]
    })
);

Logging

Unlike tools, resources, and prompts, logging is not a registered primitive — it is a handler-level API available inside any callback. Use ctx.mcpReq.log(level, data) (from {@linkcode @modelcontextprotocol/server!index.ServerContext | ServerContext}) to send structured log messages to the client. The server must declare the logging capability:

const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { logging: {} } });

Then log from any handler callback:

server.registerTool(
    'fetch-data',
    {
        description: 'Fetch data from an API',
        inputSchema: z.object({ url: z.string() })
    },
    async ({ url }, ctx): Promise<CallToolResult> => {
        await ctx.mcpReq.log('info', `Fetching ${url}`);
        const res = await fetch(url);
        await ctx.mcpReq.log('debug', `Response status: ${res.status}`);
        const text = await res.text();
        return { content: [{ type: 'text', text }] };
    }
);

Note

For logging in a full server, see simpleStreamableHttp.ts and jsonResponseStreamableHttp.ts.

For protocol details, see Logging in the MCP specification.

Server‑initiated requests

MCP is bidirectional — servers can also send requests to the client during tool execution, as long as the client declares matching capabilities.

Sampling

Use ctx.mcpReq.requestSampling(params) (from {@linkcode @modelcontextprotocol/server!index.ServerContext | ServerContext}) inside a tool handler to request an LLM completion from the connected client:

server.registerTool(
    'summarize',
    {
        description: 'Summarize text using the client LLM',
        inputSchema: z.object({ text: z.string() })
    },
    async ({ text }, ctx): Promise<CallToolResult> => {
        const response = await ctx.mcpReq.requestSampling({
            messages: [
                {
                    role: 'user',
                    content: {
                        type: 'text',
                        text: `Please summarize:\n\n${text}`
                    }
                }
            ],
            maxTokens: 500
        });
        return {
            content: [
                {
                    type: 'text',
                    text: `Model (${response.model}): ${JSON.stringify(response.content)}`
                }
            ]
        };
    }
);

Note

For a full runnable example, see toolWithSampleServer.ts.

For protocol details, see Sampling in the MCP specification.

Elicitation

Use ctx.mcpReq.elicitInput(params) (from {@linkcode @modelcontextprotocol/server!index.ServerContext | ServerContext}) inside a tool handler to request user input. Elicitation supports two modes:

  • Form (mode: 'form') — collects non‑sensitive data via a schema‑driven form.
  • URL (mode: 'url') — for sensitive data or secure web‑based flows (API keys, payments, OAuth). The client opens a URL in the browser.

Important

Sensitive information must not be collected via form elicitation; always use URL elicitation or out‑of‑band flows for secrets.

server.registerTool(
    'collect-feedback',
    {
        description: 'Collect user feedback via a form',
        inputSchema: z.object({})
    },
    async (_args, ctx): Promise<CallToolResult> => {
        const result = await ctx.mcpReq.elicitInput({
            mode: 'form',
            message: 'Please share your feedback:',
            requestedSchema: {
                type: 'object',
                properties: {
                    rating: {
                        type: 'number',
                        title: 'Rating (1\u20135)',
                        minimum: 1,
                        maximum: 5
                    },
                    comment: { type: 'string', title: 'Comment' }
                },
                required: ['rating']
            }
        });
        if (result.action === 'accept') {
            return {
                content: [
                    {
                        type: 'text',
                        text: `Thanks! ${JSON.stringify(result.content)}`
                    }
                ]
            };
        }
        return { content: [{ type: 'text', text: 'Feedback declined.' }] };
    }
);

Note

For runnable examples, see elicitationFormExample.ts (form mode) and elicitationUrlExample.ts (URL mode).

For protocol details, see Elicitation in the MCP specification.

Tasks (experimental)

Task-based execution enables "call-now, fetch-later" patterns for long-running operations. Instead of returning a result immediately, a tool creates a task that can be polled or resumed later. To use tasks:

  • Provide a {@linkcode @modelcontextprotocol/server!index.TaskStore | TaskStore} implementation that persists task metadata and results (see {@linkcode @modelcontextprotocol/server!index.InMemoryTaskStore | InMemoryTaskStore} for reference).
  • Enable the tasks capability when constructing the server.
  • Register tools with {@linkcode @modelcontextprotocol/server!experimental/tasks/mcpServer.ExperimentalMcpServerTasks#registerToolTask | server.experimental.tasks.registerToolTask(...)}.

Note

For a full runnable example, see simpleTaskInteractive.ts.

Warning

The tasks API is experimental and may change without notice.

Deployment

DNS rebinding protection

Under normal circumstances, cross-origin browser restrictions limit what a malicious website can do to your localhost server. DNS rebinding attacks get around those restrictions entirely by making the requests appear as same-origin, since the attacking domain resolves to localhost. Validating the host header on the server side protects against this scenario. All localhost MCP servers should use DNS rebinding protection.

The recommended approach is to use createMcpExpressApp() (from @modelcontextprotocol/express) or createMcpHonoApp() (from @modelcontextprotocol/hono), which enable Host header validation by default:

// Default: DNS rebinding protection auto-enabled (host is 127.0.0.1)
const app = createMcpExpressApp();

// DNS rebinding protection also auto-enabled for localhost
const appLocal = createMcpExpressApp({ host: 'localhost' });

// No automatic protection when binding to all interfaces
const appOpen = createMcpExpressApp({ host: '0.0.0.0' });

When binding to 0.0.0.0 / ::, provide an allow-list of hosts:

const app = createMcpExpressApp({
    host: '0.0.0.0',
    allowedHosts: ['localhost', '127.0.0.1', 'myhost.local']
});

createMcpHonoApp() from @modelcontextprotocol/hono provides the same protection for Hono-based servers and Web Standard runtimes (Cloudflare Workers, Deno, Bun).

If you use NodeStreamableHTTPServerTransport directly with your own HTTP framework, you must implement Host header validation yourself. See the hostHeaderValidation middleware source for reference.

More server features

The sections above cover the essentials. The table below links to additional capabilities demonstrated in the runnable examples.

Feature Description Reference
Web Standard transport Deploy on Cloudflare Workers, Deno, or Bun honoWebStandardStreamableHttp.ts
Session management Per-session transport routing, initialization, and cleanup simpleStreamableHttp.ts
Resumability Replay missed SSE events via an event store inMemoryEventStore.ts
CORS Expose MCP headers (mcp-session-id, etc.) for browser clients simpleStreamableHttp.ts
Multi‑node deployment Stateless, persistent‑storage, and distributed routing patterns examples/server/README.md