Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5958449
PoC: split away server express and hono deps into server-express and …
KKonstantinov Dec 20, 2025
354fb43
remove rate limiting from core server, move to express only
KKonstantinov Dec 20, 2025
564aed2
rename StreamableHttpServerTransport to NodeStreamableHttpServerTrans…
KKonstantinov Dec 20, 2025
5b79322
merge commit
KKonstantinov Dec 20, 2025
223fcb0
move back to hono/node-server for mapping incoming node request to we…
KKonstantinov Dec 20, 2025
aaeff28
hono-server updates
KKonstantinov Dec 20, 2025
4b7fcb0
PoC: remove sse, remove server auth, use better-auth for server auth …
KKonstantinov Dec 21, 2025
d52565e
lint fix
KKonstantinov Dec 21, 2025
62656b4
oAuth metadata: switch to better-auth/plugins -> mcp plugin
KKonstantinov Dec 21, 2025
7dc044b
working better-auth example
KKonstantinov Dec 21, 2025
8dba772
lint fix
KKonstantinov Dec 21, 2025
45f0ef1
clean up
KKonstantinov Dec 21, 2025
f5ab824
package clean up
KKonstantinov Dec 22, 2025
71297bc
update express to 5.2.1
KKonstantinov Jan 1, 2026
443abca
Merge branch 'main' into feature/v2-decouple-web-servers-from-server
KKonstantinov Jan 1, 2026
0e14e49
Merge branch 'main' into feature/v2-decouple-web-servers-from-server
KKonstantinov Jan 9, 2026
fef0b46
Merge branch 'main' into feature/v2-decouple-web-servers-from-server
KKonstantinov Jan 12, 2026
6c21c50
merge commit
KKonstantinov Jan 12, 2026
304d111
split @modelcontextprotocol/node from @modelcontextprotocol/server to…
KKonstantinov Jan 12, 2026
4c29404
lint, test fix
KKonstantinov Jan 12, 2026
ce58234
auth examples - demo flag, configs
KKonstantinov Jan 13, 2026
635c37b
package clean up, peer deps on middleware, comment fix
KKonstantinov Jan 13, 2026
c154cbb
peer deps update
KKonstantinov Jan 13, 2026
96fd273
update READMEs
KKonstantinov Jan 13, 2026
c174fe4
lint fix
KKonstantinov Jan 13, 2026
e5a8edc
merge commit
KKonstantinov Jan 13, 2026
70c348d
merge commit
KKonstantinov Jan 13, 2026
ed9c27b
fix merge
KKonstantinov Jan 13, 2026
bcb4cdb
Merge branch 'main' into feature/v2-decouple-web-servers-from-server
KKonstantinov Jan 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ jobs:
run: pnpm run build:all

- name: Publish preview packages
run: pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client'
run:
pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client'
'./packages/middleware/express' './packages/middleware/hono' './packages/middleware/node'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ dist/

# IDE
.idea/
.cursor/

# Conformance test results
results/
9 changes: 7 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,15 @@ Transports (`packages/core/src/shared/transport.ts`) provide the communication l
### Client-Side Features

- **Auth**: OAuth client support in `packages/client/src/client/auth.ts` and `packages/client/src/client/auth-extensions.ts`
- **Middleware**: Request middleware in `packages/client/src/client/middleware.ts`
- **Client middleware**: Request middleware in `packages/client/src/client/middleware.ts` (unrelated to the framework adapter packages below)
- **Sampling**: Clients can handle `sampling/createMessage` requests from servers (LLM completions)
- **Elicitation**: Clients can handle `elicitation/create` requests for user input (form or URL mode)
- **Roots**: Clients can expose filesystem roots to servers via `roots/list`

### Middleware packages (framework/runtime adapters)

The repo also ships “middleware” packages under `packages/middleware/` (e.g. `@modelcontextprotocol/express`, `@modelcontextprotocol/hono`, `@modelcontextprotocol/node`). These are thin integration layers for specific frameworks/runtimes and should not add new MCP functionality.

### Experimental Features

Located in `packages/*/src/experimental/`:
Expand Down Expand Up @@ -224,7 +228,8 @@ mcpServer.tool('tool-name', { param: z.string() }, async ({ param }, extra) => {

```typescript
// Server
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
// (Node.js IncomingMessage/ServerResponse wrapper; exported by @modelcontextprotocol/node)
const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
await server.connect(transport);

// Client
Expand Down
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# MCP TypeScript SDK

> [!IMPORTANT]
> **This is the `main` branch which contains v2 of the SDK (currently in development, pre-alpha).**
> [!IMPORTANT] **This is the `main` branch which contains v2 of the SDK (currently in development, pre-alpha).**
>
> We anticipate a stable v2 release in Q1 2026. Until then, **v1.x remains the recommended version** for production use. v1.x will continue to receive bug fixes and security updates for at least 6 months after v2 ships to give people time to upgrade.
>
Expand Down Expand Up @@ -30,6 +29,7 @@ This repository contains the TypeScript SDK implementation of the MCP specificat

- MCP **server** libraries (tools/resources/prompts, Streamable HTTP, stdio, auth helpers)
- MCP **client** libraries (transports, high-level helpers, OAuth helpers)
- Optional **middleware packages** for specific runtimes/frameworks (Express, Hono, Node.js HTTP)
- Runnable **examples** (under [`examples/`](examples/))

## Packages
Expand All @@ -41,6 +41,16 @@ This monorepo publishes split packages:

Both packages have a **required peer dependency** on `zod` for schema validation. The SDK internally imports from `zod/v4`, but remains compatible with projects using Zod v3.25+.

### Middleware packages (optional)

The SDK also publishes small “middleware” packages under [`packages/middleware/`](packages/middleware/) that help you **wire MCP into a specific runtime or web framework**.

They are intentionally thin adapters: they should not introduce new MCP functionality or business logic. See [`packages/middleware/README.md`](packages/middleware/README.md) for details.

- **`@modelcontextprotocol/node`**: Node.js Streamable HTTP transport wrapper for `IncomingMessage` / `ServerResponse`
- **`@modelcontextprotocol/express`**: Express helpers (app defaults + Host header validation)
- **`@modelcontextprotocol/hono`**: Hono helpers (app defaults + JSON body parsing hook + Host header validation)

## Installation

### Server
Expand All @@ -55,6 +65,23 @@ npm install @modelcontextprotocol/server zod
npm install @modelcontextprotocol/client zod
```

### Optional middleware packages

The SDK also publishes optional “middleware” packages that help you **wire MCP into a specific runtime or web framework** (for example Express, Hono, or Node.js `http`).

These packages are intentionally thin adapters and should not introduce additional MCP features or business logic. See [`packages/middleware/README.md`](packages/middleware/README.md) for details.

```bash
# Node.js HTTP (IncomingMessage/ServerResponse) Streamable HTTP transport:
npm install @modelcontextprotocol/node

# Express integration:
npm install @modelcontextprotocol/express express

# Hono integration:
npm install @modelcontextprotocol/hono hono
```

## Quick Start (runnable examples)

The runnable examples live under `examples/` and are kept in sync with the docs.
Expand Down
1 change: 1 addition & 0 deletions common/eslint-config/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default defineConfig(
'@typescript-eslint/consistent-type-imports': ['error', { disallowTypeAnnotations: false }],
'simple-import-sort/imports': 'warn',
'simple-import-sort/exports': 'warn',
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'import/no-extraneous-dependencies': [
'error',
{
Expand Down
8 changes: 4 additions & 4 deletions common/vitest-config/vitest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ export default defineConfig({
deps: {
moduleDirectories: ['node_modules', path.resolve(__dirname, '../../packages'), path.resolve(__dirname, '../../common')]
},
poolOptions: {
threads: {
useAtomics: true
}
},
poolOptions: {
threads: {
useAtomics: true
}
},
plugins: [tsconfigPaths()]
Expand Down
9 changes: 9 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ For production use, you can either:

The SDK ships several runnable server examples under `examples/server/src`. Start from the server examples index in [`examples/server/README.md`](../examples/server/README.md) and the entry-point quick start in the root [`README.md`](../README.md).

### Why did we remove `server` auth exports?

Server authentication & authorization is outside of the scope of the SDK, and the recommendation is to use packages that focus on this area specifically (or a full-fledged Authorization Server for those who use such). Example packages provide an example with `better-auth`.

### Why did we remove `server` SSE transport?

The SSE transport has been deprecated for a long time, and `v2` will not support it on the server side any more. Client side will keep supporting it in order to be able to connect to legacy SSE servers via the `v2` SDK, but serving SSE from `v2` will not be possible. Servers
wanting to switch to `v2` and using SSE should migrate to Streamable HTTP.

## v1 (legacy)

### Where do v1 documentation and v1-specific fixes live?
Expand Down
4 changes: 2 additions & 2 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ For more detailed patterns (stateless vs stateful, JSON response mode, CORS, DNS
MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use `createMcpExpressApp()` to create an Express app with DNS rebinding protection enabled by default:

```typescript
import { createMcpExpressApp } from '@modelcontextprotocol/server';
import { createMcpExpressApp } from '@modelcontextprotocol/express';

// Protection auto-enabled (default host is 127.0.0.1)
const app = createMcpExpressApp();
Expand All @@ -85,7 +85,7 @@ const app = createMcpExpressApp({ host: '0.0.0.0' });
When binding to `0.0.0.0` / `::`, provide an allow-list of hosts:

```typescript
import { createMcpExpressApp } from '@modelcontextprotocol/server';
import { createMcpExpressApp } from '@modelcontextprotocol/express';

const app = createMcpExpressApp({
host: '0.0.0.0',
Expand Down
7 changes: 5 additions & 2 deletions examples/server/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# MCP TypeScript SDK Examples (Server)

This directory contains runnable MCP **server** examples built with `@modelcontextprotocol/server`.
This directory contains runnable MCP **server** examples built with `@modelcontextprotocol/server` plus framework adapters:

- `@modelcontextprotocol/express`
- `@modelcontextprotocol/hono`

For client examples, see [`../client/README.md`](../client/README.md). For guided docs, see [`../../docs/server.md`](../../docs/server.md).

Expand Down Expand Up @@ -68,7 +71,7 @@ When deploying MCP servers in a horizontally scaled environment (multiple server

### Stateless mode

To enable stateless mode, configure the `StreamableHTTPServerTransport` with:
To enable stateless mode, configure the `NodeStreamableHTTPServerTransport` with:

```typescript
sessionIdGenerator: undefined;
Expand Down
6 changes: 5 additions & 1 deletion examples/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@
},
"dependencies": {
"@hono/node-server": "catalog:runtimeServerOnly",
"hono": "catalog:runtimeServerOnly",
"@modelcontextprotocol/examples-shared": "workspace:^",
"@modelcontextprotocol/node": "workspace:^",
"@modelcontextprotocol/server": "workspace:^",
"@modelcontextprotocol/express": "workspace:^",
"@modelcontextprotocol/hono": "workspace:^",
"better-auth": "^1.4.7",
"cors": "catalog:runtimeServerOnly",
"express": "catalog:runtimeServerOnly",
"hono": "catalog:runtimeServerOnly",
"zod": "catalog:runtimeShared"
},
"devDependencies": {
Expand Down
12 changes: 7 additions & 5 deletions examples/server/src/elicitationFormExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@

import { randomUUID } from 'node:crypto';

import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
import { type Request, type Response } from 'express';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';

// Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults
// The validator supports format validation (email, date, etc.) if ajv-formats is installed
Expand Down Expand Up @@ -321,7 +323,7 @@ async function main() {
const app = createMcpExpressApp();

// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};

// MCP POST endpoint
const mcpPostHandler = async (req: Request, res: Response) => {
Expand All @@ -331,13 +333,13 @@ async function main() {
}

try {
let transport: StreamableHTTPServerTransport;
let transport: NodeStreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport for this session
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request - create new transport
transport = new StreamableHTTPServerTransport({
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: sessionId => {
// Store the transport by session ID when session is initialized
Expand Down
83 changes: 17 additions & 66 deletions examples/server/src/elicitationUrlExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,16 @@

import { randomUUID } from 'node:crypto';

import { setupAuthServer } from '@modelcontextprotocol/examples-shared';
import type { CallToolResult, ElicitRequestURLParams, ElicitResult, OAuthMetadata } from '@modelcontextprotocol/server';
import {
checkResourceAllowed,
createMcpExpressApp,
createProtectedResourceMetadataRouter,
getOAuthProtectedResourceMetadataUrl,
isInitializeRequest,
mcpAuthMetadataRouter,
McpServer,
requireBearerAuth,
StreamableHTTPServerTransport,
UrlElicitationRequiredError
} from '@modelcontextprotocol/server';
setupAuthServer
} from '@modelcontextprotocol/examples-shared';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { CallToolResult, ElicitRequestURLParams, ElicitResult } from '@modelcontextprotocol/server';
import { isInitializeRequest, McpServer, UrlElicitationRequiredError } from '@modelcontextprotocol/server';
import cors from 'cors';
import type { Request, Response } from 'express';
import express from 'express';
Expand Down Expand Up @@ -238,63 +235,17 @@ let authMiddleware = null;
const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);

const oauthMetadata: OAuthMetadata = setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true });
setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true, demoMode: true });

const tokenVerifier = {
verifyAccessToken: async (token: string) => {
const endpoint = oauthMetadata.introspection_endpoint;

if (!endpoint) {
throw new Error('No token verification endpoint available in metadata');
}

const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
token: token
}).toString()
});

if (!response.ok) {
const text = await response.text().catch(() => null);
throw new Error(`Invalid or expired token: ${text}`);
}

const data = (await response.json()) as { aud: string; client_id: string; scope: string; exp: number };

if (!data.aud) {
throw new Error(`Resource Indicator (RFC8707) missing`);
}
if (!checkResourceAllowed({ requestedResource: data.aud, configuredResource: mcpServerUrl })) {
throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`);
}

// Convert the response to AuthInfo format
return {
token,
clientId: data.client_id,
scopes: data.scope ? data.scope.split(' ') : [],
expiresAt: data.exp
};
}
};
// Add metadata routes to the main MCP server
app.use(
mcpAuthMetadataRouter({
oauthMetadata,
resourceServerUrl: mcpServerUrl,
scopesSupported: ['mcp:tools'],
resourceName: 'MCP Demo Server'
})
);
// Add protected resource metadata route to the MCP server
// This allows clients to discover the auth server
app.use(createProtectedResourceMetadataRouter());

authMiddleware = requireBearerAuth({
verifier: tokenVerifier,
requiredScopes: [],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
strictResource: true,
expectedResource: mcpServerUrl
});

/**
Expand Down Expand Up @@ -594,7 +545,7 @@ app.post('/confirm-payment', express.urlencoded(), (req: Request, res: Response)
});

// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};

// Interface for a function that can send an elicitation request
type ElicitationSender = (params: ElicitRequestURLParams) => Promise<ElicitResult>;
Expand All @@ -613,15 +564,15 @@ const mcpPostHandler = async (req: Request, res: Response) => {
console.debug(`Received MCP POST for session: ${sessionId || 'unknown'}`);

try {
let transport: StreamableHTTPServerTransport;
let transport: NodeStreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
const server = getServer();
// New initialization request
const eventStore = new InMemoryEventStore();
transport = new StreamableHTTPServerTransport({
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore, // Enable resumability
onsessioninitialized: sessionId => {
Expand Down
10 changes: 6 additions & 4 deletions examples/server/src/jsonResponseStreamableHttp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { randomUUID } from 'node:crypto';

import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { CallToolResult } from '@modelcontextprotocol/server';
import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
import * as z from 'zod/v4';

Expand Down Expand Up @@ -96,21 +98,21 @@ const getServer = () => {
const app = createMcpExpressApp();

// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};

app.post('/mcp', async (req: Request, res: Response) => {
console.log('Received MCP request:', req.body);
try {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
let transport: NodeStreamableHTTPServerTransport;

if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request - use JSON response mode
transport = new StreamableHTTPServerTransport({
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: true, // Enable JSON response mode
onsessioninitialized: sessionId => {
Expand Down
Loading
Loading