Guide · SDK
MCP server SDK
The official MCP TypeScript SDK (@modelcontextprotocol/sdk) is the reference implementation of the Model Context Protocol for Node.js. It provides the McpServer class that handles the JSON-RPC message framing, session lifecycle, and protocol negotiation so your code only handles tool logic. Choosing the right transport — StreamableHTTPServerTransport for remote/containerized servers versus StdioServerTransport for local process servers — is the first decision and has permanent implications for deployment, monitoring, and client compatibility. This guide covers both from installation through production.
TL;DR
Install @modelcontextprotocol/sdk and zod. Use McpServer with StreamableHTTPServerTransport for any server that runs in a container, behind a reverse proxy, or is accessed over a network — this is the right default for production. Use StdioServerTransport only for local tools launched as child processes by the client. Register tools with server.tool(name, description, zodSchema, handler). Set up AliveMCP to probe your server every 60 seconds after deployment so any session-lifecycle regression is caught before your users hit it.
Installation and SDK versions
npm install @modelcontextprotocol/sdk zod
# For TypeScript projects also install:
npm install --save-dev typescript @types/node
The SDK follows a major.minor.patch version scheme where the major version tracks the MCP protocol specification revision. As of 2026, the stable release series is 1.x. Pin to a minor version in production ("@modelcontextprotocol/sdk": "^1.x.0") and review the changelog before upgrading — protocol spec changes may alter the initialize response shape expected by clients. If you run protocol compliance tests, a failing compliance test after an SDK upgrade means the new protocol version changed a field your clients depend on.
The SDK is ESM-only. Your project needs "type": "module" in package.json. See MCP server TypeScript setup for the full tsconfig and build configuration.
Transport selection
Transport is the first architectural decision. The SDK ships two production-ready transports:
| Transport | Use when | Client connects via |
|---|---|---|
StreamableHTTPServerTransport | Container, VPS, cloud deploy, any remote client | HTTP POST to your server's /mcp endpoint |
StdioServerTransport | Local tool, launched by the client as a child process | stdin/stdout of the child process |
Choose StreamableHTTPServerTransport for any server that runs independently of the client process. This includes everything deployed to Fly.io, Railway, Docker, Kubernetes, or a plain VPS. It is also the only transport compatible with uptime monitoring — a monitoring probe cannot connect to a stdio server because there is no network address to probe. AliveMCP and every external health-check tool require HTTP transport.
Choose StdioServerTransport only for developer tools packaged as CLI executables that a client (like Claude Desktop) launches as a child process. Stdio servers run locally, cannot be monitored externally, and die when the client process closes. They have no persistent URL, no TLS, and no way to serve multiple concurrent clients.
Minimal HTTP MCP server
// src/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { z } from 'zod';
import express from 'express';
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
server.tool(
'get_status',
'Returns the current status of a service by name',
{ service: z.string().describe('Name of the service to check') },
async (args) => {
const status = await checkService(args.service);
return { content: [{ type: 'text', text: JSON.stringify(status) }] };
}
);
const app = express();
app.use(express.json());
app.post('/mcp', async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdHeader: 'mcp-session-id',
});
await server.connect(transport);
await transport.handleRequest(req, res);
});
const PORT = parseInt(process.env.PORT ?? '3001', 10);
app.listen(PORT, () => console.log(`MCP server listening on :${PORT}`));
Each POST to /mcp creates a new transport instance for that request. The sessionIdHeader option tells the transport to read and write a session ID from the mcp-session-id HTTP header — clients use this to correlate subsequent requests to the same session. The first request in a session carries the initialize message; subsequent requests carry tools/list and tools/call messages. The session ends when the client disconnects or the connection times out.
Registering tools, resources, and prompts
The SDK exposes three registration methods on McpServer:
// Tool — executable function the AI agent calls
server.tool(
'search_docs',
'Search the documentation for a query and return matching sections',
{
query: z.string().min(1).describe('The search query'),
limit: z.number().int().min(1).max(20).default(5),
},
async (args) => {
const results = await searchDocs(args.query, args.limit);
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
}
);
// Resource — data the AI agent can read (URL-addressed)
server.resource(
'config://current',
'Current server configuration',
async (uri) => ({
contents: [{ uri: uri.href, text: JSON.stringify(getConfig()), mimeType: 'application/json' }],
})
);
// Prompt — reusable message template
server.prompt(
'summarize',
'Summarize a document',
{ document: z.string().describe('The document text to summarize') },
(args) => ({
messages: [{
role: 'user',
content: { type: 'text', text: `Summarize the following document:\n\n${args.document}` },
}],
})
);
Tools are the most commonly used primitive — they are callable functions with typed inputs. Resources expose data at a URI that clients can read without executing code. Prompts are reusable message templates that compose tool calls and user instructions. In practice, most MCP servers expose only tools. Register resources when your server has data that changes over time and clients should be able to read it directly. Register prompts only if your use case involves reusable multi-turn conversation patterns.
Session lifecycle
Every MCP session follows the same four-phase lifecycle:
- Initialize — client sends
initializewith its protocol version and capabilities. The server responds withprotocolVersion,serverInfo, and its owncapabilities. The SDK handles this automatically — you do not need to write an initialize handler. - Discovery — client calls
tools/list,resources/list, and/orprompts/listto discover what the server exposes. The SDK generates these responses from your registered tools, resources, and prompts. - Interaction — client calls
tools/call, reads resources, and uses prompts in a loop until the task is complete. - Termination — client disconnects or the session times out. No explicit close handshake is required.
The initialize handshake is what AliveMCP probes every 60 seconds. A successful probe means the server completed the initialize phase and responded with a valid protocol version. If your server fails the probe — because it crashed, a dependency is down, or the network path is broken — AliveMCP triggers an alert before any user session hits the broken initialize step. See MCP server health check for the exact probe logic.
Stdio server for local tools
// src/index.ts — stdio variant for local CLI tools
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({ name: 'local-tool', version: '1.0.0' });
server.tool(
'read_file',
'Read a file from the local filesystem',
{ path: z.string().describe('Absolute path to the file') },
async (args) => {
const { readFile } = await import('node:fs/promises');
const content = await readFile(args.path, 'utf-8');
return { content: [{ type: 'text', text: content }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
// The server stays alive until stdin closes
For stdio servers, the transport reads JSON-RPC messages from stdin and writes responses to stdout. All logging must go to stderr — writing anything other than valid JSON-RPC to stdout corrupts the protocol stream. Use console.error() instead of console.log() for debug output. Stdio servers cannot be externally monitored — if you need uptime visibility, use HTTP transport instead.
Related questions
Which version of the MCP SDK should I use?
Use the latest 1.x stable release. Pin to the exact minor version you tested against ("@modelcontextprotocol/sdk": "1.x.y") and upgrade intentionally. The major version tracks the protocol specification — a 2.x release would be a protocol-breaking change. Check the SDK changelog before any upgrade and run your protocol compliance tests after upgrading. If your clients pin to a specific protocol version, verify that the new SDK version still supports it in the initialize handshake.
Can I use the MCP SDK from Python or Go?
There are community SDKs for Python (mcp on PyPI, maintained by Anthropic) and Go. The TypeScript SDK is the reference implementation with the most complete feature coverage. The Python SDK follows the same McpServer pattern with FastMCP as the high-level API. The Go SDK is lower-level and requires more manual message handling. Protocol compliance tests written as raw JSON-RPC HTTP requests work against any language implementation — the protocol is language-agnostic.
How do I handle multiple tools with shared state?
Define shared state as module-level variables or inject a context object at server construction time. Each tool handler is a closure over that state. The SDK does not provide a per-session context mechanism at the tool level — if you need per-session state (e.g., a user-specific auth token), store it in a Map keyed on the session ID from the mcp-session-id header and pass it into tool handlers via middleware. Clean up session state on session termination by listening for the transport's close event. See MCP server authentication for how session-bound auth tokens are typically managed.
Does the SDK handle reconnection automatically?
No. The SDK handles a single session per transport instance. Reconnection is the client's responsibility — the client sends a new initialize request to start a fresh session. On the server side, each new transport instance is independent. If you need to resume state across reconnections (e.g., long-running tool execution), you must implement that yourself using a shared store (Redis, a database) keyed on a client-provided session token, and expose a resume_session tool that the client calls after reconnecting.
Further reading
- MCP server TypeScript — type-safe tool handlers with Zod and the official SDK
- MCP server authentication — securing access with API keys and OAuth 2.0
- MCP server versioning — protocol negotiation and backward-compatible schema changes
- MCP server testing — protocol compliance and schema snapshot tests
- MCP server deployment — containerized HTTP transport on Fly.io, Railway, and Docker
- MCP server health check — what the initialize probe verifies
- AliveMCP — uptime monitoring for every MCP endpoint