Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .changeset/modern-spec-cimd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"mcp-handler": major
---

Upgrade to MCP SDK v2 and the 2026-07-28 MCP specification (CIMD era).

- The handler now serves the stateless 2026-07-28 protocol (per-request `_meta` envelope, `server/discover`) natively, with the SDK's stateless legacy fallback answering 2025-era Streamable HTTP clients from the same handler.
- **Breaking**: requires `@modelcontextprotocol/server` ^2.0.0 (replaces the `@modelcontextprotocol/sdk` peer dependency), `zod` ^4.2.0 for schemas, and Node.js 20+.
- **Breaking**: the legacy HTTP+SSE transport (protocol 2024-11-05) has been removed. `/sse` and `/message` endpoints answer `410 Gone`; the `redis` dependency and `redisUrl`, `maxDuration`, and `sessionIdGenerator` config options are deprecated no-ops.
- **Breaking**: tool/prompt/resource registration follows SDK v2 (`registerTool` with `z.object(...)` Standard Schemas; variadic `server.tool(...)` is gone; `extra.authInfo` is now `ctx.http?.authInfo`).
- `withMcpAuth` now builds its 401/403 challenges with the SDK's consolidated `OAuthError`/`bearerAuthChallengeResponse`, keeping RFC 9728 `resource_metadata` discovery in place for CIMD-era authorization flows. Dynamic Client Registration is deprecated by the spec in favor of Client ID Metadata Documents — see README.
20 changes: 15 additions & 5 deletions .github/workflows/release-snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,28 @@ jobs:
with:
token: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
package-manager-cache: false

- name: Install npm with trusted publishing support
run: npm install --global npm@11

- name: Verify publishing runtime
run: |
node --version
npm --version

- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-setup
with:
version: 9.4.0
run_install: false

- name: Add npm auth token to pnpm
run: pnpm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN_ELEVATED}"
env:
NPM_TOKEN_ELEVATED: ${{secrets.NPM_TOKEN_ELEVATED}}
- name: Install Dependencies
id: pnpm-install
run: pnpm install --frozen-lockfile
Expand All @@ -49,4 +60,3 @@ jobs:
pnpm changeset publish --no-git-tag --tag snapshot
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }}
40 changes: 31 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

A Vercel adapter for the Model Context Protocol (MCP), enabling real-time communication between your applications and AI models. Supports Next.js and Nuxt.

Built on MCP SDK v2, serving the **2026-07-28** MCP specification (stateless protocol, `server/discover`, CIMD-era authorization) while transparently falling back to stateless Streamable HTTP for 2025-era clients — one handler, both protocol generations.

## Installation

```bash
npm install mcp-handler @modelcontextprotocol/sdk@1.26.0 zod@^3
npm install mcp-handler @modelcontextprotocol/server zod
```

> **Note**: Versions of `@modelcontextprotocol/sdk` prior to 1.26.0 have a security vulnerability. Use version 1.26.0 or later.
> **Note**: `mcp-handler` 2.x requires the MCP SDK v2 packages (`@modelcontextprotocol/server` ^2.0.0), zod ^4.2.0, and Node.js 20+. If you're on `@modelcontextprotocol/sdk` 1.x, use `mcp-handler` 1.x.

## Quick Start (Next.js)

Expand All @@ -24,9 +26,9 @@ const handler = createMcpHandler(
{
title: "Roll Dice",
description: "Roll a dice with a specified number of sides.",
inputSchema: {
inputSchema: z.object({
sides: z.number().int().min(2),
},
}),
},
async ({ sides }) => {
const value = 1 + Math.floor(Math.random() * sides);
Expand All @@ -39,7 +41,6 @@ const handler = createMcpHandler(
{},
{
basePath: "/api", // must match where [transport] is located
maxDuration: 60,
verboseLogs: true,
}
);
Expand Down Expand Up @@ -70,6 +71,29 @@ For stdio-only clients, use [mcp-remote](https://www.npmjs.com/package/mcp-remot
}
```

## Protocol Support

- **2026-07-28** (current): served natively — stateless, no sessions, per-request `_meta` envelope, `server/discover`.
- **2025-era Streamable HTTP**: served via the SDK's stateless legacy fallback from the same handler. GET/DELETE session operations answer `405` (serving is stateless).
- **HTTP+SSE transport (2024-11-05)**: removed in 2.x. Requests to the `/sse` and `/message` endpoints answer `410 Gone`. Redis is no longer needed or used.

### Authorization (CIMD era)

The 2026-07-28 spec deprecates Dynamic Client Registration (DCR) in favor of **Client ID Metadata Documents (CIMD)**, where the OAuth client identifies itself with an HTTPS URL that serves its metadata. CIMD support is advertised and implemented by your **authorization server** (`client_id_metadata_document_supported` in its RFC 8414 metadata); this package keeps your MCP server's resource-server surface up to date:

- `withMcpAuth` verifies bearer tokens and answers `401`/`403` with RFC 9728-compliant `WWW-Authenticate` challenges pointing at your protected resource metadata.
- `protectedResourceHandler` serves the RFC 9728 Protected Resource Metadata document listing your authorization servers.

See [Authorization](docs/AUTHORIZATION.md) for wiring details.

## Migrating from 1.x

- Install `@modelcontextprotocol/server` (v2) and `zod@^4`; remove `@modelcontextprotocol/sdk` and `redis`.
- `inputSchema`/`argsSchema` take a full Standard Schema (e.g. `z.object({ ... })`) instead of a raw zod shape.
- Variadic `server.tool(...)` / `.prompt(...)` / `.resource(...)` are removed — use `registerTool` / `registerPrompt` / `registerResource`.
- In handler callbacks, `extra.authInfo` is now `ctx.http?.authInfo`.
- Config options `redisUrl`, `maxDuration`, `sseEndpoint`, `sseMessageEndpoint`, and `sessionIdGenerator` are deprecated no-ops.

## Documentation

- [Client Integration](docs/CLIENTS.md) - Claude Desktop, Cursor, Windsurf setup
Expand All @@ -79,15 +103,13 @@ For stdio-only clients, use [mcp-remote](https://www.npmjs.com/package/mcp-remot
## Features

- **Framework Support**: Next.js and Nuxt
- **Multiple Transports**: Streamable HTTP and Server-Sent Events (SSE)
- **Redis Integration**: Optional, for SSE transport resumability
- **Dual-era protocol support**: 2026-07-28 (stateless) and 2025-era Streamable HTTP from one handler
- **TypeScript Support**: Full type definitions included

## Requirements

- Next.js 13+ or Nuxt 3+
- Node.js 18+
- Redis (optional, for SSE)
- Node.js 20+

## License

Expand Down
12 changes: 5 additions & 7 deletions docs/ADVANCED.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const handler = async (
{
title: "Roll Dice",
description: "Roll a dice with a specified number of sides.",
inputSchema: { sides: z.number().int().min(2) },
inputSchema: z.object({ sides: z.number().int().min(2) }),
},
async ({ sides }) => {
const value = 1 + Math.floor(Math.random() * sides);
Expand All @@ -41,10 +41,8 @@ const handler = async (
},
},
{
redisUrl: process.env.REDIS_URL,
basePath: `/dynamic/${p}`,
verboseLogs: true,
maxDuration: 60,
}
)(req);
};
Expand All @@ -56,10 +54,10 @@ export { handler as GET, handler as POST, handler as DELETE };

```typescript
interface Config {
redisUrl?: string; // Redis connection URL for pub/sub
basePath?: string; // Base path for MCP endpoints
maxDuration?: number; // Maximum duration for SSE connections (seconds)
basePath?: string; // Base path for MCP endpoints
verboseLogs?: boolean; // Enable debug logging
onEvent?: (event: McpEvent) => void; // Analytics/debugging callback
disableSse?: boolean; // Respond 404 instead of 410 on removed SSE endpoints
}
```

Expand All @@ -78,7 +76,7 @@ const handler = createMcpHandler(
{
title: "Roll Dice",
description: "Roll a dice with a specified number of sides.",
inputSchema: { sides: z.number().int().min(2) },
inputSchema: z.object({ sides: z.number().int().min(2) }),
},
async ({ sides }) => {
const value = 1 + Math.floor(Math.random() * sides);
Expand Down
21 changes: 13 additions & 8 deletions docs/AUTHORIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The MCP adapter supports the [MCP Authorization Specification](https://modelcont

```typescript
// app/api/[transport]/route.ts
import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
import type { AuthInfo } from "@modelcontextprotocol/server";
import { createMcpHandler, withMcpAuth } from "mcp-handler";
import { z } from "zod";

Expand All @@ -17,18 +17,17 @@ const handler = createMcpHandler(
{
title: "Echo",
description: "Echo a message",
inputSchema: { message: z.string() },
inputSchema: z.object({ message: z.string() }),
},
async ({ message }, extra) => {
// Access auth info via extra.authInfo
async ({ message }, ctx) => {
// Access auth info via ctx.http?.authInfo
const authInfo = ctx.http?.authInfo;
return {
content: [
{
type: "text",
text: `Echo: ${message}${
extra.authInfo?.token
? ` for user ${extra.authInfo.clientId}`
: ""
authInfo?.token ? ` for user ${authInfo.clientId}` : ""
}`,
},
],
Expand Down Expand Up @@ -99,4 +98,10 @@ The path should match `resourceMetadataPath` in your `withMcpAuth` config (defau
2. `verifyToken` validates the token and returns auth info
3. If auth is required and fails → 401 response
4. If required scopes are missing → 403 response
5. On success, auth info is available via `extra.authInfo` in tool handlers
5. On success, auth info is available via `ctx.http?.authInfo` in tool handlers

## CIMD (Client ID Metadata Documents)

The 2026-07-28 MCP spec deprecates Dynamic Client Registration (DCR) in favor of CIMD: OAuth clients identify themselves with an HTTPS URL (`client_id`) that serves their metadata document, removing the need for a registration round-trip.

CIMD is implemented by the **authorization server**, which advertises it via `client_id_metadata_document_supported: true` in its RFC 8414 metadata. As a resource server, your MCP deployment doesn't change beyond what this package already provides — clients discover your authorization servers through the Protected Resource Metadata endpoint above, then negotiate CIMD (or fall back to DCR during the deprecation window) directly with the authorization server. If you operate your own authorization server, enable CIMD there; DCR remains functional for backward compatibility but will be removed in a future spec revision.
21 changes: 14 additions & 7 deletions docs/CLIENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Restart Claude Desktop to pick up changes. You should see a hammer icon in the b

Edit `~/.cursor/mcp.json`.

As of version 0.48.0, Cursor supports unauthed SSE servers directly. If your MCP server uses OAuth authorization, you still need mcp-remote.
Cursor supports Streamable HTTP servers directly. If your MCP server uses OAuth authorization, you may still need mcp-remote.

## Windsurf

Expand All @@ -75,11 +75,18 @@ Edit `~/.codeium/windsurf/mcp_config.json`.
Use the MCP client directly in your application:

```typescript
import { McpClient } from "@modelcontextprotocol/sdk/client";

const client = new McpClient({
transport: new SSEClientTransport("/api/mcp/mcp"),
import {
Client,
StreamableHTTPClientTransport,
} from "@modelcontextprotocol/client";

const client = new Client({ name: "my-app", version: "1.0.0" });
await client.connect(
new StreamableHTTPClientTransport(new URL("https://example.com/api/mcp/mcp"))
);

const result = await client.callTool({
name: "yourTool",
arguments: { param: "value" },
});

const result = await client.request("yourMethod", { param: "value" });
```
32 changes: 11 additions & 21 deletions examples/auth/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types";
import type { AuthInfo } from "@modelcontextprotocol/server";
import {
createMcpHandler,
withMcpAuth,
Expand All @@ -8,44 +8,34 @@ import { z } from "zod";
// Define the handler with proper parameter validation
const handler = createMcpHandler(
(server) => {
server.tool(
server.registerTool(
"echo",
"Echo a message back with authentication info",
{
message: z.string().describe("The message to echo back"),
description: "Echo a message back with authentication info",
inputSchema: z.object({
message: z.string().describe("The message to echo back"),
}),
},
async ({ message }, extra) => {
async ({ message }, ctx) => {
const authInfo = ctx.http?.authInfo;
return {
content: [
{
type: "text",
text: `Echo: ${message}${
extra.authInfo?.token
? ` (from ${extra.authInfo.clientId})`
: ""
authInfo?.token ? ` (from ${authInfo.clientId})` : ""
}`,
},
],
};
}
);
},
// Server capabilities
{
capabilities: {
auth: {
type: "bearer",
required: true,
},
},
},
// Server options
{},
// Route configuration
{
streamableHttpEndpoint: "/mcp",
sseEndpoint: "/sse",
sseMessageEndpoint: "/message",
basePath: "/api/mcp",
redisUrl: process.env.REDIS_URL,
}
);

Expand Down
39 changes: 19 additions & 20 deletions examples/route.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
import createMcpRouteHandler from '../dist/next/index';
import { createMcpHandler } from 'mcp-handler';

const handler = createMcpRouteHandler(
const handler = createMcpHandler(
server => {
server.tool('echo', 'Echo a message', {}, async () => {
return {
content: [
{
type: 'text',
text: 'Hello, world!',
},
],
};
});
server.registerTool(
'echo',
{ description: 'Echo a message' },
async () => {
return {
content: [
{
type: 'text',
text: 'Hello, world!',
},
],
};
}
);
},
// Optional: Comes from the McpServer.options
{},
// Optional: Comes from the createMcpHandler config
{
capabilities: {},
},
// Optional: Comes from the createMcpRouteHandler config
{
streamableHttpEndpoint: '/mcp',
sseEndpoint: '/sse',
sseMessageEndpoint: '/message',
basePath: '/api/mcp',
redisUrl: process.env.REDIS_URL,
verboseLogs: true,
}
);

Expand Down
Loading
Loading