MCP TypeScript SDK examples

July 6, 2026 · View on GitHub

One story per directory. Every story is a runnable, self-verifying client/server pair: server.ts is what you would deploy, client.ts is what a host would write — it connects, exercises the feature with the public client API, asserts results, and exits 0. CI runs every pair over every transport it supports (scripts/examples/run-examples.ts); a non-zero exit fails the build.

Each story is its own private workspace package (@mcp-examples/<story>). Run any pair from the repo root:

# stdio (the client spawns the server itself):
pnpm --filter @mcp-examples/<story> client

# Streamable HTTP (two terminals):
pnpm --filter @mcp-examples/<story> server -- --http --port 3000
pnpm --filter @mcp-examples/<story> client -- --http http://127.0.0.1:3000/mcp

Add -- --legacy to the client command for the 2025-era handshake.

Every HTTP leg serves the handler behind host/origin validation — via createMcpHonoApp() (or another framework app factory, which arm the checks by default on localhost binds), or, for raw node:http stories, via the localhostHostValidation() / localhostOriginValidation() guards from @modelcontextprotocol/node composed in front of toNodeHandler on an explicit loopback bind. New stories follow the canonical skeleton — see CONTRIBUTING.md.

The one exception to the generic commands is the reference pair: cli-client/ and todos-server/ have their own entry points (pnpm --filter @mcp-examples/cli-client start, pnpm --filter @mcp-examples/todos-server start:http) — see their READMEs.

Start here

StoryWhat it teaches
tools/Register tools, infer input/output schemas, call them, structured output
prompts/Prompts + argument completion
resources/Static + templated resources, list/read, era-split subscriptions
dual-era/One factory, both protocol eras, both transports

Feature stories

StoryWhat it teachesTransportsEra
mrtr/Multi-round-trip write-once tool, secure requestStatestdio + httpmodern
subscriptions/subscriptions/listen: client.listen() + auto-open, handler.notify / ServerEventBusstdio + httpmodern
streaming/In-flight progress, logging, cancellationstdio + httpdual
elicitation/Elicitation (form + URL mode), both eras: push-style on 2025, inputRequired on 2026stdio + httpdual
sampling/Tool that requests LLM sampling from the client, both eras: push-style on 2025, inputRequired on 2026stdio + httpdual
stickynotes/"Real app" capstone: tools mutate state, a resource per note, listChanged, elicitation-confirmed clearstdio + httpdual
cli-client/Reference host: LLM chat CLI with provider seam — tool loop, @-mention resources, prompt commands, sampling, elicitation, roots, OAuth, cancellationstdio + httpdual
todos-server/Reference server (pairs with cli-client): every server feature with a real job — CRUD tools, sampling, multi-round elicitation, subscriptions, progressstdio + httpdual
caching/cacheHints stamping on cacheable results (2026-07-28)stdio + httpmodern
gateway/connect({ prior }) — probe once, zero-round-trip connect for every worker (gateway pattern)httpmodern
custom-methods/Vendor-prefixed methods + custom notificationsstdio + httpdual
extension-capabilities/Declaring capabilities.extensions and reading the negotiated mapstdio + httpdual
schema-validators/ArkType, Valibot, Zod, and outputSchemastdio + httpdual
custom-version/supportedProtocolVersions / version negotiationstdio + httplegacy
parallel-calls/Multiple clients / parallel tool calls, per-client notificationsstdio + httpdual
legacy-routing/isLegacyRequest in front of an existing sessionful 1.x deployment + a strict modern entry on one porthttpdual (in-body)
bearer-auth/Resource server with bearer token; 401 + WWW-Authenticatehttpdual
bearer-auth-web/Web-standard twin: host/origin guards + requireBearerAuth + createMcpHandler as one fetch handlerhttpdual
oauth/OAuth authorization_code: in-repo AS (auto-consent) + headless redirect-following clienthttpdual
oauth-client-credentials/OAuth client_credentials (machine-to-machine): in-repo AS + ClientCredentialsProviderhttpdual
scoped-tools/Per-tool scope on createMcpHandler — bearer-verify gate + handler-level ctx.http?.authInfo checkshttpmodern

HTTP hosting variants

StoryWhat it teachesTransportsEra
stateless-legacy/createMcpHandler default posture (the minimal deployment)httpdual (in-body)
json-response/createMcpHandler({ responseMode: 'json' })httpmodern
hono/createMcpHandler(...).fetch on Hono / web-standard runtimeshttpdual
sse-polling/SEP-1699 SSE polling/resumption (sessionful 2025)httplegacy
standalone-get/Standalone GET stream + listChanged push (sessionful 2025)httplegacy

dual (in-body) = the client connects to both eras inside one runner invocation; the story demonstrates one server serving both side by side.

Excluded

DirectoryWhat it isWhy not in CI
repl/Fully-featured HTTP playground server + readline clientInteractive — client.ts reads from stdin. Run manually in two terminals.
guides/Per-page snippet companions synced into the docs/ guide pagesNot a client/server story pair; typechecked and executed by pnpm docs:examples.
server-quickstart/, client-quickstart/Standalone starter projects (the original quickstart sources)External network / API key; typecheck-only.
shared/Argv/assert scaffold (parseExampleArgs/check/siblingPath); demo OAuth provider + InMemoryEventStore at the ./auth subpathNot a story — imported by every story as scaffolding.

Multi-node deployment patterns

When deploying MCP servers in a horizontally scaled environment (multiple server instances), there are a few different options that can be useful for different use cases:

  • Stateless mode - no need to maintain state between calls.
  • Persistent storage mode - state stored in a database; any node can handle a session.
  • Local state with message routing - stateful nodes + pub/sub routing for a session.

Stateless mode

To enable stateless mode, configure the NodeStreamableHTTPServerTransport with:

sessionIdGenerator: undefined;
┌─────────────────────────────────────────────┐
│                  Client                     │
└─────────────────────────────────────────────┘


┌─────────────────────────────────────────────┐
│                Load Balancer                │
└─────────────────────────────────────────────┘
          │                       │
          ▼                       ▼
┌─────────────────┐     ┌─────────────────────┐
│  MCP Server #1  │     │    MCP Server #2    │
│ (Node.js)       │     │  (Node.js)          │
└─────────────────┘     └─────────────────────┘

Persistent storage mode

Configure the transport with session management, but use an external event store:

sessionIdGenerator: () => randomUUID(),
eventStore: databaseEventStore
┌─────────────────────────────────────────────┐
│                  Client                     │
└─────────────────────────────────────────────┘


┌─────────────────────────────────────────────┐
│                Load Balancer                │
└─────────────────────────────────────────────┘
          │                       │
          ▼                       ▼
┌─────────────────┐     ┌─────────────────────┐
│  MCP Server #1  │     │    MCP Server #2    │
│ (Node.js)       │     │  (Node.js)          │
└─────────────────┘     └─────────────────────┘
          │                       │
          │                       │
          ▼                       ▼
┌─────────────────────────────────────────────┐
│           Database (PostgreSQL)             │
│                                             │
│  • Session state                            │
│  • Event storage for resumability           │
└─────────────────────────────────────────────┘

Streamable HTTP with distributed message routing

For scenarios where local in-memory state must be maintained on specific nodes, combine Streamable HTTP with pub/sub routing so one node can terminate the client connection while another node owns the session state.

┌─────────────────────────────────────────────┐
│                  Client                     │
└─────────────────────────────────────────────┘


┌─────────────────────────────────────────────┐
│                Load Balancer                │
└─────────────────────────────────────────────┘
          │                       │
          ▼                       ▼
┌─────────────────┐     ┌─────────────────────┐
│  MCP Server #1  │◄───►│    MCP Server #2    │
│ (Has Session A) │     │  (Has Session B)    │
└─────────────────┘     └─────────────────────┘
          ▲│                     ▲│
          │▼                     │▼
┌─────────────────────────────────────────────┐
│         Message Queue / Pub-Sub             │
│                                             │
│  • Session ownership registry               │
│  • Bidirectional message routing            │
│  • Request/response forwarding              │
└─────────────────────────────────────────────┘

Backwards compatibility (Streamable HTTP ↔ legacy SSE)

A client that needs to fall back from Streamable HTTP to the legacy HTTP+SSE transport (for servers that only implement the older transport) follows Fall back to SSE for servers that predate Streamable HTTP in the client docs — try StreamableHTTPClientTransport first, fall back to SSEClientTransport on a 4xx. There is no runnable pair for this in examples/ (the legacy SSE server transport is deprecated); the connect_sseFallback snippet in guides/clients/connect.examples.ts is the complete pattern.