MCP TypeScript SDK: Complete API Reference 2026

Full API reference for the MCP TypeScript SDK — servers, tools, resources, prompts, transports, and auth. Build production MCP integrations fast.

What the MCP TypeScript SDK Covers

The Model Context Protocol TypeScript SDK (@modelcontextprotocol/sdk) is the official library for building MCP servers and clients in TypeScript and Node.js. It handles the JSON-RPC 2.0 transport layer, capability negotiation, and message routing — so you focus on tools, resources, and prompts.

This reference covers every major API surface in SDK v1.x (current as of March 2026).

You'll learn:

  • How to create servers with tools, resources, and prompts
  • Transport options: stdio, HTTP/SSE, and in-process
  • Client API for connecting to MCP servers
  • Auth, error handling, and production patterns

Time: 25 min | Difficulty: Intermediate


Installation

# npm
npm install @modelcontextprotocol/sdk

# pnpm
pnpm add @modelcontextprotocol/sdk

# Verify install
node -e "const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js'); console.log('ok')"

Minimum requirements: Node.js 18+, TypeScript 5.0+ (if using TS)

// tsconfig.json minimum config
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "strict": true
  }
}

Server API

McpServer

McpServer is the high-level class for building MCP servers. It wraps the lower-level Server class and provides ergonomic .tool(), .resource(), and .prompt() registration methods.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = new McpServer({
  name: "my-server",      // shown to clients during handshake
  version: "1.0.0",       // semver string
});

Constructor Options

OptionTypeRequiredDescription
namestringServer name shown to clients
versionstringServer version (semver)
capabilitiesServerCapabilitiesExplicitly declare capabilities (usually auto-inferred)

server.tool()

Register a callable tool. Tools are the primary way MCP servers expose functionality to LLMs.

import { z } from "zod";

server.tool(
  "get_weather",                          // tool name
  "Get current weather for a city",       // description (shown to LLM)
  {
    city: z.string().describe("City name, e.g. 'Tokyo'"),
    units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
  },
  async ({ city, units }) => {
    const data = await fetchWeather(city, units);
    return {
      content: [
        {
          type: "text",
          text: `${city}: ${data.temp}°${units === "celsius" ? "C" : "F"}, ${data.condition}`,
        },
      ],
    };
  }
);

tool() Signature

server.tool(
  name: string,
  description: string,
  schema: ZodRawShape,          // Zod schema object (NOT z.object() — raw shape)
  handler: (args, extra) => Promise<CallToolResult>
): void

The extra parameter contains request metadata:

server.tool("my-tool", "desc", { q: z.string() }, async (args, extra) => {
  console.log(extra.requestId);   // unique request ID
  console.log(extra.signal);      // AbortSignal — respect this for cancellation
  return { content: [{ type: "text", text: "done" }] };
});

CallToolResult Shape

// Text response
return {
  content: [{ type: "text", text: "your response here" }],
};

// Image response
return {
  content: [{ type: "image", data: base64String, mimeType: "image/png" }],
};

// Resource response (reference to a resource URI)
return {
  content: [{ type: "resource", resource: { uri: "file:///path/to/file", text: "content" } }],
};

// Multiple content blocks
return {
  content: [
    { type: "text", text: "Here is the chart:" },
    { type: "image", data: chartBase64, mimeType: "image/png" },
  ],
};

// Signal error to the LLM (not a thrown exception)
return {
  content: [{ type: "text", text: "City not found" }],
  isError: true,
};

server.resource()

Register a static or dynamic resource. Resources expose data that clients and LLMs can read by URI.

Static Resource

server.resource(
  "app-config",                        // resource name
  "config://app",                      // URI — clients use this to read it
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        text: JSON.stringify({ version: "1.0", env: "production" }),
        mimeType: "application/json",
      },
    ],
  })
);

Dynamic Resource with URI Template

import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";

server.resource(
  "user-profile",
  new ResourceTemplate("users://{userId}/profile", { list: undefined }),
  async (uri, { userId }) => ({
    contents: [
      {
        uri: uri.href,
        text: JSON.stringify(await db.users.findById(userId)),
        mimeType: "application/json",
      },
    ],
  })
);

ResourceTemplate Options

new ResourceTemplate(
  "pattern://{param}",   // URI template — {param} is extracted and passed to handler
  {
    list: async () => ({           // optional: enables resource listing
      resources: [
        { uri: "users://123/profile", name: "Alice" },
        { uri: "users://456/profile", name: "Bob" },
      ],
    }),
  }
);

server.prompt()

Register a prompt template. Prompts let servers expose reusable message templates to LLM clients.

server.prompt(
  "debug-error",
  "Generate a debugging prompt for a given error",
  {
    error: z.string().describe("The error message or stack trace"),
    language: z.string().optional().describe("Programming language"),
  },
  ({ error, language }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `Debug this ${language ?? "code"} error:\n\n${error}\n\nExplain the root cause and provide a fix.`,
        },
      },
    ],
  })
);

Prompt Handler Return Shape

{
  description?: string;        // optional override shown to client
  messages: PromptMessage[];   // one or more messages
}

// PromptMessage
{
  role: "user" | "assistant";
  content: TextContent | ImageContent | EmbeddedResource;
}

Low-Level Server API

Use Server directly when you need manual control over request handling — for proxies, middleware, or custom protocol extensions.

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
  { name: "low-level-server", version: "1.0.0" },
  {
    capabilities: {
      tools: {},          // declare tool capability
      resources: {},      // declare resource capability
      prompts: {},        // declare prompt capability
    },
  }
);

// Handle tool list requests
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "ping",
      description: "Returns pong",
      inputSchema: { type: "object", properties: {}, required: [] },
    },
  ],
}));

// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "ping") {
    return { content: [{ type: "text", text: "pong" }] };
  }
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
});

Transport API

StdioServerTransport

The standard transport for CLI tools and local integrations (used by Cursor, Claude Desktop, etc.).

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const transport = new StdioServerTransport();
await server.connect(transport);
// Server now listens on stdin, writes to stdout

No configuration needed. The process must not write anything else to stdout — use stderr for logs.

// ✅ Log to stderr, never stdout
console.error("Server started");   // ok
console.log("Server started");     // breaks the transport

StreamableHTTPServerTransport

HTTP transport with optional SSE streaming. Use for web-based MCP deployments.

import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const app = express();
app.use(express.json());

// Stateless mode — new transport per request (simplest, recommended for REST)
app.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,   // undefined = stateless
  });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000);

Stateful Mode (SSE streaming)

import { randomUUID } from "crypto";

const sessions = new Map<string, StreamableHTTPServerTransport>();

app.post("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;

  if (sessionId && sessions.has(sessionId)) {
    // Reuse existing session
    await sessions.get(sessionId)!.handleRequest(req, res, req.body);
    return;
  }

  // New session
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => randomUUID(),
    onsessioninitialized: (id) => sessions.set(id, transport),
  });

  transport.onclose = () => sessions.delete(transport.sessionId!);

  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

// SSE stream endpoint
app.get("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;
  const transport = sessions.get(sessionId);
  if (!transport) return res.status(404).send("Session not found");
  await transport.handleRequest(req, res);
});

SSEServerTransport (Legacy)

The older SSE-only transport. Use StreamableHTTPServerTransport for new projects. Kept here for compatibility with older clients.

import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";

app.get("/sse", (req, res) => {
  const transport = new SSEServerTransport("/message", res);
  server.connect(transport);
});

app.post("/message", (req, res) => {
  transport.handlePostMessage(req, res);
});

Client API

Client

Use Client to connect to an MCP server from your own application — useful for testing, orchestration, and multi-agent setups.

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "node",
  args: ["./my-server.js"],
  env: { ...process.env },    // pass env to subprocess
});

const client = new Client(
  { name: "my-client", version: "1.0.0" },
  { capabilities: {} }        // declare what the client supports
);

await client.connect(transport);

client.listTools()

const { tools } = await client.listTools();

for (const tool of tools) {
  console.log(tool.name, tool.description);
  // tool.inputSchema — JSON Schema of expected arguments
}

client.callTool()

const result = await client.callTool({
  name: "get_weather",
  arguments: { city: "Tokyo", units: "celsius" },
});

for (const block of result.content) {
  if (block.type === "text") console.log(block.text);
}

if (result.isError) {
  console.error("Tool reported an error");
}

client.listResources()

const { resources } = await client.listResources();
// resources[].uri, resources[].name, resources[].mimeType

client.readResource()

const { contents } = await client.readResource({ uri: "config://app" });

for (const content of contents) {
  if ("text" in content) console.log(content.text);
}

client.listPrompts() / client.getPrompt()

const { prompts } = await client.listPrompts();

const result = await client.getPrompt({
  name: "debug-error",
  arguments: { error: "TypeError: Cannot read property 'x' of undefined" },
});

// result.messages — array of PromptMessage ready to send to an LLM

Client Transports

StdioClientTransport

Spawns a local process and communicates over stdin/stdout.

import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
  env: process.env as Record<string, string>,
  stderr: "pipe",   // "pipe" | "inherit" | "ignore"
});

StreamableHTTPClientTransport

Connect to an HTTP MCP server.

import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(
  new URL("http://localhost:3000/mcp"),
  {
    requestInit: {
      headers: { Authorization: `Bearer ${token}` },
    },
  }
);

SSEClientTransport (Legacy)

import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";

const transport = new SSEClientTransport(
  new URL("http://localhost:3000/sse")
);

Error Handling

McpError

Throw McpError inside handlers to return structured JSON-RPC errors to the client.

import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";

server.tool("get_user", "Get user by ID", { id: z.string() }, async ({ id }) => {
  const user = await db.users.findById(id);

  if (!user) {
    // Option 1: Signal soft error to LLM (tool ran, but result is an error)
    return {
      content: [{ type: "text", text: `User ${id} not found` }],
      isError: true,
    };
  }

  return { content: [{ type: "text", text: JSON.stringify(user) }] };
});

// Option 2: Throw for protocol-level errors (invalid params, server fault)
server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (!req.params.name) {
    throw new McpError(ErrorCode.InvalidParams, "Tool name is required");
  }
});

Standard Error Codes

CodeConstantWhen to use
-32700ErrorCode.ParseErrorInvalid JSON received
-32600ErrorCode.InvalidRequestRequest shape is wrong
-32601ErrorCode.MethodNotFoundUnknown method or tool name
-32602ErrorCode.InvalidParamsArguments fail validation
-32603ErrorCode.InternalErrorUnhandled server exception

Rule of thumb: use isError: true in content when the tool ran but got a bad result. Throw McpError only when the request itself is malformed.


Authentication

The SDK doesn't enforce auth — you add it at the transport layer.

API Key (HTTP)

app.post("/mcp", (req, res, next) => {
  const key = req.headers["x-api-key"];
  if (key !== process.env.MCP_API_KEY) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  next();
}, async (req, res) => {
  // handle MCP request
});

OAuth 2.0 with OAuthServerProvider

For full OAuth support, the SDK exports OAuthServerProvider:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { OAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/provider.js";

// Implement the provider interface
class MyOAuthProvider implements OAuthServerProvider {
  async authorize(req, res) { /* redirect to auth */ }
  async token(req, res) { /* exchange code for token */ }
  async verifyAccessToken(token: string) {
    const payload = await verifyJwt(token);
    return {
      token,
      clientId: payload.sub,
      scopes: payload.scopes,
      expiresAt: payload.exp,
    };
  }
  // ... clientsStore, revoke etc.
}

// Attach to server
const provider = new MyOAuthProvider();
// Pass provider when creating transport or route middleware

For a full OAuth example, see the MCP OAuth example in the SDK repo.


Progress and Logging

Progress Notifications

Long-running tools can report progress back to the client:

server.tool(
  "process_files",
  "Process a batch of files",
  { paths: z.array(z.string()) },
  async ({ paths }, extra) => {
    for (let i = 0; i < paths.length; i++) {
      // Check for cancellation before each unit of work
      extra.signal.throwIfAborted();

      await processFile(paths[i]);

      // Report progress — client can show a progress bar
      await server.server.notification({
        method: "notifications/progress",
        params: {
          progressToken: extra.requestId,
          progress: i + 1,
          total: paths.length,
        },
      });
    }

    return { content: [{ type: "text", text: `Processed ${paths.length} files` }] };
  }
);

Logging

Use the MCP logging notification to send structured logs to the client (avoids polluting stdout):

await server.server.notification({
  method: "notifications/message",
  params: {
    level: "info",           // "debug" | "info" | "warning" | "error"
    logger: "my-server",
    data: { message: "Connected to database", host: dbHost },
  },
});

Resource Change Notifications

When your resources update, notify connected clients so they can re-read:

// Notify a specific resource changed
await server.server.notification({
  method: "notifications/resources/updated",
  params: { uri: "config://app" },
});

// Notify the resource list itself changed (new or removed resources)
await server.server.notification({
  method: "notifications/resources/list_changed",
  params: {},
});

// Same pattern for tools and prompts
await server.server.notification({ method: "notifications/tools/list_changed", params: {} });
await server.server.notification({ method: "notifications/prompts/list_changed", params: {} });

In-Process Transport (Testing)

Skip the network entirely — connect a client and server in the same process. Ideal for unit tests.

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";

const server = new McpServer({ name: "test-server", version: "0.0.1" });
server.tool("add", "Add two numbers", { a: z.number(), b: z.number() }, async ({ a, b }) => ({
  content: [{ type: "text", text: String(a + b) }],
}));

const client = new Client({ name: "test-client", version: "0.0.1" }, { capabilities: {} });

// Create linked transport pair
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

await Promise.all([
  client.connect(clientTransport),
  server.connect(serverTransport),
]);

// Now test your tools directly
const result = await client.callTool({ name: "add", arguments: { a: 2, b: 3 } });
console.log(result.content[0].text); // "5"

await client.close();

TypeScript Types Reference

Key types exported from @modelcontextprotocol/sdk/types.js:

// Tool result content blocks
type TextContent = { type: "text"; text: string };
type ImageContent = { type: "image"; data: string; mimeType: string };
type EmbeddedResource = { type: "resource"; resource: TextResourceContents | BlobResourceContents };

// Resource contents
type TextResourceContents = { uri: string; text: string; mimeType?: string };
type BlobResourceContents = { uri: string; blob: string; mimeType?: string };  // blob = base64

// Prompt message
type PromptMessage = { role: "user" | "assistant"; content: TextContent | ImageContent | EmbeddedResource };

// Tool call result
type CallToolResult = { content: (TextContent | ImageContent | EmbeddedResource)[]; isError?: boolean };

// Error
class McpError extends Error {
  constructor(code: ErrorCode, message: string, data?: unknown);
  code: number;
  data?: unknown;
}

Production Checklist

Server

  • Use StdioServerTransport for CLI / local tools, StreamableHTTPServerTransport for web
  • Write logs to stderr, never stdout (stdio transport)
  • Respect extra.signal (AbortSignal) in long-running tools
  • Return isError: true for tool-level errors, throw McpError for protocol errors
  • Add try/catch around external calls inside handlers — unhandled throws crash the server
  • Test with InMemoryTransport before wiring up real transports

Client

  • Always await client.close() on shutdown — flushes pending messages
  • Handle result.isError === true in callTool responses
  • Cache listTools() result — don't call it on every tool invocation
  • Set stderr: "pipe" on StdioClientTransport so subprocess errors don't leak to your stdout

What You Learned

  • McpServer with .tool(), .resource(), .prompt() covers 90% of use cases
  • StdioServerTransport for local tools; StreamableHTTPServerTransport for web APIs
  • isError: true in content vs throwing McpError serve different purposes
  • InMemoryTransport makes server testing fast and reliable
  • Progress, logging, and list-change notifications require calling server.server.notification() directly

Next step: If you're building a tool-heavy server, check out the MCP server examples for filesystem, GitHub, and database reference implementations.

Tested on @modelcontextprotocol/sdk v1.8.0, Node.js 22.x, TypeScript 5.7