MCP TypeScript SDK (V2)
    Preparing search index...

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

    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 stateful setup:

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

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

    await server.connect(transport);
    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.

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

    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.

    For local, process‑spawned integrations (Claude Desktop, CLI tools), use StdioServerTransport:

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

    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:

    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.

    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.

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

    Both prompts and resources can support argument completions. Wrap a field in the argsSchema with 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.`
    }
    }
    ]
    })
    );

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

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

    Use ctx.mcpReq.requestSampling(params) (from 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.

    Use ctx.mcpReq.elicitInput(params) (from 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.

    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:

    Note

    For a full runnable example, see simpleTaskInteractive.ts.

    Warning

    The tasks API is experimental and may change without notice.

    MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use createMcpExpressApp() from @modelcontextprotocol/express to create an Express app with DNS rebinding protection enabled 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']
    });

    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