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
| Story | What 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
| Story | What it teaches | Transports | Era |
|---|---|---|---|
mrtr/ | Multi-round-trip write-once tool, secure requestState | stdio + http | modern |
subscriptions/ | subscriptions/listen: client.listen() + auto-open, handler.notify / ServerEventBus | stdio + http | modern |
streaming/ | In-flight progress, logging, cancellation | stdio + http | dual |
elicitation/ | Elicitation (form + URL mode), both eras: push-style on 2025, inputRequired on 2026 | stdio + http | dual |
sampling/ | Tool that requests LLM sampling from the client, both eras: push-style on 2025, inputRequired on 2026 | stdio + http | dual |
stickynotes/ | "Real app" capstone: tools mutate state, a resource per note, listChanged, elicitation-confirmed clear | stdio + http | dual |
cli-client/ | Reference host: LLM chat CLI with provider seam — tool loop, @-mention resources, prompt commands, sampling, elicitation, roots, OAuth, cancellation | stdio + http | dual |
todos-server/ | Reference server (pairs with cli-client): every server feature with a real job — CRUD tools, sampling, multi-round elicitation, subscriptions, progress | stdio + http | dual |
caching/ | cacheHints stamping on cacheable results (2026-07-28) | stdio + http | modern |
gateway/ | connect({ prior }) — probe once, zero-round-trip connect for every worker (gateway pattern) | http | modern |
custom-methods/ | Vendor-prefixed methods + custom notifications | stdio + http | dual |
extension-capabilities/ | Declaring capabilities.extensions and reading the negotiated map | stdio + http | dual |
schema-validators/ | ArkType, Valibot, Zod, and outputSchema | stdio + http | dual |
custom-version/ | supportedProtocolVersions / version negotiation | stdio + http | legacy |
parallel-calls/ | Multiple clients / parallel tool calls, per-client notifications | stdio + http | dual |
legacy-routing/ | isLegacyRequest in front of an existing sessionful 1.x deployment + a strict modern entry on one port | http | dual (in-body) |
bearer-auth/ | Resource server with bearer token; 401 + WWW-Authenticate | http | dual |
bearer-auth-web/ | Web-standard twin: host/origin guards + requireBearerAuth + createMcpHandler as one fetch handler | http | dual |
oauth/ | OAuth authorization_code: in-repo AS (auto-consent) + headless redirect-following client | http | dual |
oauth-client-credentials/ | OAuth client_credentials (machine-to-machine): in-repo AS + ClientCredentialsProvider | http | dual |
scoped-tools/ | Per-tool scope on createMcpHandler — bearer-verify gate + handler-level ctx.http?.authInfo checks | http | modern |
HTTP hosting variants
| Story | What it teaches | Transports | Era |
|---|---|---|---|
stateless-legacy/ | createMcpHandler default posture (the minimal deployment) | http | dual (in-body) |
json-response/ | createMcpHandler({ responseMode: 'json' }) | http | modern |
hono/ | createMcpHandler(...).fetch on Hono / web-standard runtimes | http | dual |
sse-polling/ | SEP-1699 SSE polling/resumption (sessionful 2025) | http | legacy |
standalone-get/ | Standalone GET stream + listChanged push (sessionful 2025) | http | legacy |
dual (in-body) = the client connects to both eras inside one runner invocation; the story demonstrates one server serving both side by side.
Excluded
| Directory | What it is | Why not in CI |
|---|---|---|
repl/ | Fully-featured HTTP playground server + readline client | Interactive — client.ts reads from stdin. Run manually in two terminals. |
guides/ | Per-page snippet companions synced into the docs/ guide pages | Not 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 subpath | Not 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.