diff --git a/.changeset/modern-spec-cimd.md b/.changeset/modern-spec-cimd.md new file mode 100644 index 0000000..89e7021 --- /dev/null +++ b/.changeset/modern-spec-cimd.md @@ -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. diff --git a/.github/workflows/release-snapshot.yml b/.github/workflows/release-snapshot.yml index 2323a3d..187eac9 100644 --- a/.github/workflows/release-snapshot.yml +++ b/.github/workflows/release-snapshot.yml @@ -24,6 +24,21 @@ 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 @@ -31,10 +46,6 @@ jobs: 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 @@ -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 }} diff --git a/README.md b/README.md index 9884081..a990979 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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); @@ -39,7 +41,6 @@ const handler = createMcpHandler( {}, { basePath: "/api", // must match where [transport] is located - maxDuration: 60, verboseLogs: true, } ); @@ -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 @@ -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 diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index a51e385..d72c01a 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -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); @@ -41,10 +41,8 @@ const handler = async ( }, }, { - redisUrl: process.env.REDIS_URL, basePath: `/dynamic/${p}`, verboseLogs: true, - maxDuration: 60, } )(req); }; @@ -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 } ``` @@ -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); diff --git a/docs/AUTHORIZATION.md b/docs/AUTHORIZATION.md index f982ee7..8aa3c75 100644 --- a/docs/AUTHORIZATION.md +++ b/docs/AUTHORIZATION.md @@ -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"; @@ -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}` : "" }`, }, ], @@ -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. diff --git a/docs/CLIENTS.md b/docs/CLIENTS.md index 720da50..5082596 100644 --- a/docs/CLIENTS.md +++ b/docs/CLIENTS.md @@ -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 @@ -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" }); ``` diff --git a/examples/auth/route.ts b/examples/auth/route.ts index b81a12a..f767d8f 100644 --- a/examples/auth/route.ts +++ b/examples/auth/route.ts @@ -1,4 +1,4 @@ -import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types"; +import type { AuthInfo } from "@modelcontextprotocol/server"; import { createMcpHandler, withMcpAuth, @@ -8,21 +8,22 @@ 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})` : "" }`, }, ], @@ -30,22 +31,11 @@ const handler = createMcpHandler( } ); }, - // 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, } ); diff --git a/examples/route.ts b/examples/route.ts index cdd4f7a..dd93643 100644 --- a/examples/route.ts +++ b/examples/route.ts @@ -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, } ); diff --git a/package.json b/package.json index 661b6b9..24186a5 100644 --- a/package.json +++ b/package.json @@ -57,29 +57,33 @@ "license": "Apache-2.0", "dependencies": { "chalk": "^5.3.0", - "commander": "^11.1.0", - "redis": "^4.6.0" + "commander": "^11.1.0" }, "devDependencies": { "@changesets/cli": "^2.27.12", + "@modelcontextprotocol/client": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", "@types/node": "^22.15.8", "tsup": "^8.0.0", "typescript": "^5.0.0", "vitest": "^3.2.1", - "zod": "^3.25.50" + "zod": "^4.4.3" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "1.26.0", + "@modelcontextprotocol/server": "^2.0.0", "next": ">=13.0.0" }, "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { + "@modelcontextprotocol/server": { "optional": false }, "next": { "optional": true } }, + "engines": { + "node": ">=20" + }, "packageManager": "pnpm@9.4.0", "publishConfig": { "access": "public", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5222620..ec5b3b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@modelcontextprotocol/sdk': - specifier: 1.26.0 - version: 1.26.0(zod@3.25.50) chalk: specifier: ^5.3.0 version: 5.4.1 @@ -20,13 +17,16 @@ importers: next: specifier: '>=13.0.0' version: 13.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - redis: - specifier: ^4.6.0 - version: 4.6.0 devDependencies: '@changesets/cli': specifier: ^2.27.12 version: 2.29.4 + '@modelcontextprotocol/client': + specifier: ^2.0.0 + version: 2.0.0 + '@modelcontextprotocol/server': + specifier: ^2.0.0 + version: 2.0.0 '@types/node': specifier: ^22.15.8 version: 22.15.8 @@ -40,8 +40,8 @@ importers: specifier: ^3.2.1 version: 3.2.1(@types/node@22.15.8)(yaml@2.8.0) zod: - specifier: ^3.25.50 - version: 3.25.50 + specifier: ^4.4.3 + version: 4.4.3 packages: @@ -392,12 +392,6 @@ packages: cpu: [x64] os: [win32] - '@hono/node-server@1.19.11': - resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -426,15 +420,17 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - '@modelcontextprotocol/sdk@1.26.0': - resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true + '@modelcontextprotocol/client@2.0.0': + resolution: {integrity: sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==} + engines: {node: '>=20'} + + '@modelcontextprotocol/core@2.0.0': + resolution: {integrity: sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==} + engines: {node: '>=20'} + + '@modelcontextprotocol/server@2.0.0': + resolution: {integrity: sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==} + engines: {node: '>=20'} '@next/env@13.0.0': resolution: {integrity: sha512-65v9BVuah2Mplohm4+efsKEnoEuhmlGm8B2w6vD1geeEP2wXtlSJCvR/cCRJ3fD8wzCQBV41VcMBQeYET6MRkg==} @@ -533,35 +529,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@redis/bloom@1.2.0': - resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/client@1.5.0': - resolution: {integrity: sha512-MafrVNQ5LSnii1yYIlzjbz3i6Bw8c9RZkV3g7jNd3As/A1vo6UIn0+29ii631oKxIA+nAEMeACh6+9k9+iup1A==} - engines: {node: '>=14'} - - '@redis/graph@1.1.0': - resolution: {integrity: sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/json@1.0.4': - resolution: {integrity: sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/search@1.1.1': - resolution: {integrity: sha512-pqCXTc5e7wJJgUuJiC3hBgfoFRoPxYzwn0BEfKgejTM7M/9zP3IpUcqcjgfp8hF+LoV8rHZzcNTz7V+pEIY7LQ==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/time-series@1.0.4': - resolution: {integrity: sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==} - peerDependencies: - '@redis/client': ^1.0.0 - '@rollup/rollup-android-arm-eabi@4.40.2': resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==} cpu: [arm] @@ -709,21 +676,6 @@ packages: '@vitest/utils@3.2.1': resolution: {integrity: sha512-KkHlGhePEKZSub5ViknBcN5KEF+u7dSUr9NW8QsVICusUojrgrOnnY3DEWWO877ax2Pyopuk2qHmt+gkNKnBVw==} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -773,10 +725,6 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} - engines: {node: '>=18'} - brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -790,22 +738,10 @@ packages: peerDependencies: esbuild: '>=0.17' - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - caniuse-lite@1.0.30001718: resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} @@ -835,10 +771,6 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} - engines: {node: '>=0.10.0'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -854,26 +786,6 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} - engines: {node: '>= 0.6'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -887,23 +799,10 @@ packages: supports-color: optional: true - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -912,45 +811,22 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - esbuild@0.19.12: resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} engines: {node: '>=12'} @@ -961,9 +837,6 @@ packages: engines: {node: '>=18'} hasBin: true - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -972,10 +845,6 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - eventsource-parser@3.0.2: resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==} engines: {node: '>=18.0.0'} @@ -992,16 +861,6 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} - express-rate-limit@8.3.1: - resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -1009,16 +868,10 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1034,10 +887,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} - engines: {node: '>= 0.8'} - find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -1046,14 +895,6 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -1067,21 +908,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - generic-pool@3.9.0: - resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} - engines: {node: '>= 4'} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -1098,33 +924,9 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hono@4.12.9: - resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} - engines: {node: '>=16.9.0'} - - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - human-id@4.1.1: resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} hasBin: true @@ -1137,29 +939,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} - engines: {node: '>= 12'} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1180,9 +963,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1215,12 +995,6 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-schema-typed@8.0.2: - resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -1258,18 +1032,6 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1281,14 +1043,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} - mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -1316,10 +1070,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - next@13.0.0: resolution: {integrity: sha512-puH1WGM6rGeFOoFdXXYfUxN9Sgi4LMytCV5HkQJvVUOhHfC1DoVqOfvzaEteyp6P04IW+gbtK2Q9pInVSrltPA==} engines: {node: '>=14.6.0'} @@ -1350,17 +1100,6 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -1398,10 +1137,6 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1414,10 +1149,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@8.2.0: - resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} - engines: {node: '>=16'} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -1477,40 +1208,16 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} - engines: {node: '>=0.6'} - - qs@6.15.0: - resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} - engines: {node: '>=0.6'} - quansync@0.2.10: resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} - engines: {node: '>= 0.8'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -1528,13 +1235,6 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - redis@4.6.0: - resolution: {integrity: sha512-QTlRvQtfRM9ZxFRfBmd8UsGGrKZil/cAwlbDTnaSKirCUMfWgXjKQ4VlkhmNBc3mnAbbSgeYw9zs/HcfYGd1Cw==} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1548,16 +1248,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1569,17 +1262,6 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} - - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} - engines: {node: '>= 18'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1588,22 +1270,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1635,14 +1301,6 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -1729,10 +1387,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -1765,10 +1419,6 @@ packages: typescript: optional: true - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - typescript@5.0.2: resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==} engines: {node: '>=12.20'} @@ -1781,19 +1431,11 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vite-node@3.2.1: resolution: {integrity: sha512-V4EyKQPxquurNJPtQJRZo8hKOoKNBRIhxcDbQFPFig0JdoWcUhwRgK8yoCXXrfYVPKS6XwirGHPszLnR8FbjCA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1891,24 +1533,13 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.8.0: resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} engines: {node: '>= 14.6'} hasBin: true - zod-to-json-schema@3.25.1: - resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} - peerDependencies: - zod: ^3.25 || ^4 - - zod@3.25.50: - resolution: {integrity: sha512-VstOnRxf4tlSq0raIwbn0n+LA34SxVoZ8r3pkwSUM0jqNiA/HCMQEVjTuS5FZmHsge+9MDGTiAuHyml5T0um6A==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} snapshots: @@ -2200,10 +1831,6 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@hono/node-server@1.19.11(hono@4.12.9)': - dependencies: - hono: 4.12.9 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2246,27 +1873,24 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@modelcontextprotocol/sdk@1.26.0(zod@3.25.50)': + '@modelcontextprotocol/client@2.0.0': dependencies: - '@hono/node-server': 1.19.11(hono@4.12.9) - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.5 + '@modelcontextprotocol/core': 2.0.0 cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.0.2 - express: 5.2.1 - express-rate-limit: 8.3.1(express@5.2.1) - hono: 4.12.9 jose: 6.1.3 - json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 - raw-body: 3.0.0 - zod: 3.25.50 - zod-to-json-schema: 3.25.1(zod@3.25.50) - transitivePeerDependencies: - - supports-color + zod: 4.4.3 + + '@modelcontextprotocol/core@2.0.0': + dependencies: + zod: 4.4.3 + + '@modelcontextprotocol/server@2.0.0': + dependencies: + '@modelcontextprotocol/core': 2.0.0 + zod: 4.4.3 '@next/env@13.0.0': {} @@ -2324,32 +1948,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@redis/bloom@1.2.0(@redis/client@1.5.0)': - dependencies: - '@redis/client': 1.5.0 - - '@redis/client@1.5.0': - dependencies: - cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - - '@redis/graph@1.1.0(@redis/client@1.5.0)': - dependencies: - '@redis/client': 1.5.0 - - '@redis/json@1.0.4(@redis/client@1.5.0)': - dependencies: - '@redis/client': 1.5.0 - - '@redis/search@1.1.1(@redis/client@1.5.0)': - dependencies: - '@redis/client': 1.5.0 - - '@redis/time-series@1.0.4(@redis/client@1.5.0)': - dependencies: - '@redis/client': 1.5.0 - '@rollup/rollup-android-arm-eabi@4.40.2': optional: true @@ -2469,22 +2067,6 @@ snapshots: loupe: 3.1.3 tinyrainbow: 2.0.0 - accepts@2.0.0: - dependencies: - mime-types: 3.0.1 - negotiator: 1.0.0 - - ajv-formats@3.0.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -2520,20 +2102,6 @@ snapshots: binary-extensions@2.3.0: {} - body-parser@2.2.2: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.3 - http-errors: 2.0.0 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.15.0 - raw-body: 3.0.2 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 @@ -2547,20 +2115,8 @@ snapshots: esbuild: 0.19.12 load-tsconfig: 0.2.5 - bytes@3.1.2: {} - cac@6.7.14: {} - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - caniuse-lite@1.0.30001718: {} chai@5.2.0: @@ -2593,8 +2149,6 @@ snapshots: client-only@0.0.1: {} - cluster-key-slot@1.1.2: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2605,21 +2159,6 @@ snapshots: commander@4.1.1: {} - content-disposition@1.0.0: - dependencies: - safe-buffer: 5.2.1 - - content-type@1.0.5: {} - - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - - cors@2.8.5: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2630,51 +2169,27 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.3: - dependencies: - ms: 2.1.3 - deep-eql@5.0.2: {} - depd@2.0.0: {} - detect-indent@6.1.0: {} dir-glob@3.0.1: dependencies: path-type: 4.0.0 - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - eastasianwidth@0.2.0: {} - ee-first@1.1.1: {} - emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} - enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - es-module-lexer@1.7.0: {} - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - esbuild@0.19.12: optionalDependencies: '@esbuild/aix-ppc64': 0.19.12 @@ -2729,16 +2244,12 @@ snapshots: '@esbuild/win32-ia32': 0.25.5 '@esbuild/win32-x64': 0.25.5 - escape-html@1.0.3: {} - esprima@4.0.1: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.7 - etag@1.8.1: {} - eventsource-parser@3.0.2: {} eventsource@3.0.7: @@ -2759,44 +2270,6 @@ snapshots: expect-type@1.2.1: {} - express-rate-limit@8.3.1(express@5.2.1): - dependencies: - express: 5.2.1 - ip-address: 10.1.0 - - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.2 - content-disposition: 1.0.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.1 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.0 - fresh: 2.0.0 - http-errors: 2.0.0 - merge-descriptors: 2.0.0 - mime-types: 3.0.1 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.0 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 - statuses: 2.0.1 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - extendable-error@0.1.7: {} external-editor@3.1.0: @@ -2805,8 +2278,6 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 - fast-deep-equal@3.1.3: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2815,8 +2286,6 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-uri@3.1.0: {} - fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -2829,17 +2298,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.0: - dependencies: - debug: 4.4.1 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -2850,10 +2308,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - forwarded@0.2.0: {} - - fresh@2.0.0: {} - fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -2869,28 +2323,6 @@ snapshots: fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - - generic-pool@3.9.0: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - get-stream@6.0.1: {} glob-parent@5.1.2: @@ -2915,34 +2347,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - gopd@1.2.0: {} - graceful-fs@4.2.11: {} - has-symbols@1.1.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hono@4.12.9: {} - - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - human-id@4.1.1: {} human-signals@2.1.0: {} @@ -2951,22 +2357,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - ignore@5.3.2: {} - inherits@2.0.4: {} - - ip-address@10.1.0: {} - - ipaddr.js@1.9.1: {} - is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -2981,8 +2373,6 @@ snapshots: is-number@7.0.0: {} - is-promise@4.0.0: {} - is-stream@2.0.1: {} is-subdir@1.2.0: @@ -3010,10 +2400,6 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 - json-schema-traverse@1.0.0: {} - - json-schema-typed@8.0.2: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -3044,12 +2430,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - math-intrinsics@1.1.0: {} - - media-typer@1.1.0: {} - - merge-descriptors@2.0.0: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -3059,12 +2439,6 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.54.0: {} - - mime-types@3.0.1: - dependencies: - mime-db: 1.54.0 - mimic-fn@2.1.0: {} minimatch@9.0.5: @@ -3085,8 +2459,6 @@ snapshots: nanoid@3.3.11: {} - negotiator@1.0.0: {} - next@13.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 13.0.0 @@ -3123,16 +2495,6 @@ snapshots: object-assign@4.1.1: {} - object-inspect@1.13.4: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -3163,8 +2525,6 @@ snapshots: dependencies: quansync: 0.2.10 - parseurl@1.3.3: {} - path-exists@4.0.0: {} path-key@3.1.1: {} @@ -3174,8 +2534,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@8.2.0: {} - path-type@4.0.0: {} pathe@2.0.3: {} @@ -3215,41 +2573,12 @@ snapshots: prettier@2.8.8: {} - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - punycode@2.3.1: {} - qs@6.14.0: - dependencies: - side-channel: 1.1.0 - - qs@6.15.0: - dependencies: - side-channel: 1.1.0 - quansync@0.2.10: {} queue-microtask@1.2.3: {} - range-parser@1.2.1: {} - - raw-body@3.0.0: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 - unpipe: 1.0.0 - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -3271,17 +2600,6 @@ snapshots: dependencies: picomatch: 2.3.1 - redis@4.6.0: - dependencies: - '@redis/bloom': 1.2.0(@redis/client@1.5.0) - '@redis/client': 1.5.0 - '@redis/graph': 1.1.0(@redis/client@1.5.0) - '@redis/json': 1.0.4(@redis/client@1.5.0) - '@redis/search': 1.1.1(@redis/client@1.5.0) - '@redis/time-series': 1.0.4(@redis/client@1.5.0) - - require-from-string@2.0.2: {} - resolve-from@5.0.0: {} reusify@1.1.0: {} @@ -3312,22 +2630,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.40.2 fsevents: 2.3.3 - router@2.2.0: - dependencies: - debug: 4.4.1 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.2.0 - transitivePeerDependencies: - - supports-color - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - safe-buffer@5.2.1: {} - safer-buffer@2.1.2: {} scheduler@0.23.2: @@ -3336,67 +2642,12 @@ snapshots: semver@7.7.2: {} - send@1.2.0: - dependencies: - debug: 4.4.1 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.0 - mime-types: 3.0.1 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.0: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.0 - transitivePeerDependencies: - - supports-color - - setprototypeof@1.2.0: {} - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -3420,10 +2671,6 @@ snapshots: stackback@0.0.2: {} - statuses@2.0.1: {} - - statuses@2.0.2: {} - std-env@3.9.0: {} string-width@4.2.3: @@ -3498,8 +2745,6 @@ snapshots: dependencies: is-number: 7.0.0 - toidentifier@1.0.1: {} - tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -3533,26 +2778,16 @@ snapshots: - supports-color - ts-node - type-is@2.0.1: - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.1 - typescript@5.0.2: {} undici-types@6.21.0: {} universalify@0.1.2: {} - unpipe@1.0.0: {} - use-sync-external-store@1.2.0(react@18.3.1): dependencies: react: 18.3.1 - vary@1.1.2: {} - vite-node@3.2.1(@types/node@22.15.8)(yaml@2.8.0): dependencies: cac: 6.7.14 @@ -3657,14 +2892,6 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 - wrappy@1.0.2: {} - - yallist@4.0.0: {} - yaml@2.8.0: {} - zod-to-json-schema@3.25.1(zod@3.25.50): - dependencies: - zod: 3.25.50 - - zod@3.25.50: {} + zod@4.4.3: {} diff --git a/src/auth/auth-context.ts b/src/auth/auth-context.ts index 79bac24..f806632 100644 --- a/src/auth/auth-context.ts +++ b/src/auth/auth-context.ts @@ -1,4 +1,4 @@ -import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; +import type { AuthInfo } from "@modelcontextprotocol/server"; import { AsyncLocalStorage } from "node:async_hooks"; const authContext = new AsyncLocalStorage(); diff --git a/src/auth/auth-metadata.ts b/src/auth/auth-metadata.ts index d06b28d..dcf37de 100644 --- a/src/auth/auth-metadata.ts +++ b/src/auth/auth-metadata.ts @@ -1,4 +1,4 @@ -import { OAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/shared/auth.js"; +import type { OAuthProtectedResourceMetadata } from "@modelcontextprotocol/server"; import { getPublicUrl } from "../lib/url"; /** diff --git a/src/auth/auth-wrapper.ts b/src/auth/auth-wrapper.ts index be7c096..639391f 100644 --- a/src/auth/auth-wrapper.ts +++ b/src/auth/auth-wrapper.ts @@ -1,11 +1,11 @@ -import {AuthInfo} from "@modelcontextprotocol/sdk/server/auth/types.js"; +import type { AuthInfo } from "@modelcontextprotocol/server"; import { - InvalidTokenError, - InsufficientScopeError, - ServerError, -} from "@modelcontextprotocol/sdk/server/auth/errors.js"; -import {withAuthContext} from "./auth-context"; -import {getPublicOrigin} from "../lib/url"; + OAuthError, + OAuthErrorCode, + bearerAuthChallengeResponse, +} from "@modelcontextprotocol/server"; +import { withAuthContext } from "./auth-context"; +import { getPublicOrigin } from "../lib/url"; declare global { interface Request { @@ -43,6 +43,7 @@ export function withMcpAuth( return async (req: Request) => { const origin = resourceUrl ?? getPublicOrigin(req); const resourceMetadataUrl = `${origin}${resourceMetadataPath}`; + const challengeOptions = { requiredScopes, resourceMetadataUrl }; const authHeader = req.headers.get("Authorization"); const [type, token] = authHeader?.split(" ") || []; @@ -56,19 +57,18 @@ export function withMcpAuth( authInfo = await verifyToken(req, bearerToken); } catch (error) { console.error("Unexpected error authenticating bearer token:", error); - const publicError = new InvalidTokenError("Invalid token"); - return new Response(JSON.stringify(publicError.toResponseObject()), { - status: 401, - headers: { - "WWW-Authenticate": `Bearer error="${publicError.errorCode}", error_description="${publicError.message}", resource_metadata="${resourceMetadataUrl}"`, - "Content-Type": "application/json", - }, - }); + return bearerAuthChallengeResponse( + new OAuthError(OAuthErrorCode.InvalidToken, "Invalid token"), + challengeOptions + ); } try { if (required && !authInfo) { - throw new InvalidTokenError("No authorization provided"); + throw new OAuthError( + OAuthErrorCode.InvalidToken, + "No authorization provided" + ); } if (!authInfo) { @@ -82,13 +82,16 @@ export function withMcpAuth( ); if (!hasAllScopes) { - throw new InsufficientScopeError("Insufficient scope"); + throw new OAuthError( + OAuthErrorCode.InsufficientScope, + "Insufficient scope" + ); } } // Check if the token is expired if (authInfo.expiresAt && authInfo.expiresAt < Date.now() / 1000) { - throw new InvalidTokenError("Token has expired"); + throw new OAuthError(OAuthErrorCode.InvalidToken, "Token has expired"); } // Set auth info on the request object after successful verification @@ -96,39 +99,13 @@ export function withMcpAuth( return withAuthContext(authInfo, () => handler(req)); } catch (error) { - if (error instanceof InvalidTokenError) { - return new Response(JSON.stringify(error.toResponseObject()), { - status: 401, - headers: { - "WWW-Authenticate": `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`, - "Content-Type": "application/json", - }, - }); - } else if (error instanceof InsufficientScopeError) { - return new Response(JSON.stringify(error.toResponseObject()), { - status: 403, - headers: { - "WWW-Authenticate": `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`, - "Content-Type": "application/json", - }, - }); - } else if (error instanceof ServerError) { - return new Response(JSON.stringify(error.toResponseObject()), { - status: 500, - headers: { - "Content-Type": "application/json", - }, - }); - } else { + if (!OAuthError.isInstance(error)) { console.error("Unexpected error authenticating bearer token:", error); - const serverError = new ServerError("Internal Server Error"); - return new Response(JSON.stringify(serverError.toResponseObject()), { - status: 500, - headers: { - "Content-Type": "application/json", - }, - }); } + // Maps invalid_token → 401, insufficient_scope → 403 (both with a + // WWW-Authenticate challenge advertising resource_metadata), anything + // unexpected → 500 server_error. + return bearerAuthChallengeResponse(error, challengeOptions); } }; } diff --git a/src/cli/index.ts b/src/cli/index.ts index e3d27ca..bb3919c 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -12,11 +12,13 @@ import { z } from 'zod'; const handler = createMcpHandler( server => { - server.tool( + server.registerTool( 'roll_dice', - 'Rolls an N-sided die', - { - sides: z.number().int().min(2) + { + description: 'Rolls an N-sided die', + inputSchema: z.object({ + sides: z.number().int().min(2), + }), }, async ({ sides }) => { const value = 1 + Math.floor(Math.random() * sides); @@ -30,11 +32,8 @@ const handler = createMcpHandler( // Optional server options }, { - // Optional redis config - redisUrl: process.env.REDIS_URL, // You need these endpoints basePath: '/api', - maxDuration: 60, verboseLogs: true, } ); @@ -66,7 +65,7 @@ async function installDependencies( packageManager: "npm" | "pnpm" | "yarn" | "bun" ) { const execSync = (await import("node:child_process")).execSync; - const dependencies = ["mcp-handler", "zod"]; + const dependencies = ["mcp-handler", "@modelcontextprotocol/server", "zod"]; const commands = { npm: `npm install ${dependencies.join(" ")}`, diff --git a/src/handler/index.ts b/src/handler/index.ts index 2f1c01f..0affd81 100644 --- a/src/handler/index.ts +++ b/src/handler/index.ts @@ -1,7 +1,8 @@ import { type Config, initializeMcpApiHandler } from "./mcp-api-handler"; -import { createServerResponseAdapter } from "./server-response-adapter"; -import type { ServerOptions as McpServerOptions } from "@modelcontextprotocol/sdk/server/index.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { + ServerOptions as McpServerOptions, + McpServer, +} from "@modelcontextprotocol/server"; /** * Creates a MCP handler that can be used to handle MCP requests. @@ -25,14 +26,5 @@ export default function createMcpRouteHandler( serverOptions?: ServerOptions, config?: Config ): (request: Request) => Promise { - const mcpHandler = initializeMcpApiHandler( - initializeServer, - serverOptions, - config - ); - return (request: Request) => { - return createServerResponseAdapter(request.signal, (res) => { - mcpHandler(request, res); - }); - }; + return initializeMcpApiHandler(initializeServer, serverOptions, config); } diff --git a/src/handler/mcp-api-handler.ts b/src/handler/mcp-api-handler.ts index 80999cf..964d85f 100644 --- a/src/handler/mcp-api-handler.ts +++ b/src/handler/mcp-api-handler.ts @@ -1,34 +1,10 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { - type IncomingHttpHeaders, - IncomingMessage, - type ServerResponse, -} from "node:http"; -import { createClient } from "redis"; -import { Socket } from "node:net"; -import { Readable } from "node:stream"; -import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; -import type { BodyType } from "./server-response-adapter"; -import assert from "node:assert"; -import type { - McpEvent, -} from "../lib/log-helper"; -import { EventEmittingResponse } from "../lib/event-emitter.js"; -import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types"; -import { getAuthContext } from "../auth/auth-context"; -import { ServerOptions } from "."; - -interface SerializedRequest { - requestId: string; - url: string; - method: string; - body: BodyType; - headers: IncomingHttpHeaders; - auth?: AuthInfo; -} - -type LogLevel = "log" | "error" | "warn" | "info" | "debug"; + createMcpHandler as createSdkMcpHandler, + McpServer, +} from "@modelcontextprotocol/server"; +import type { McpEvent, McpRequestEvent, McpErrorEvent } from "../lib/log-helper"; +import { createEvent } from "../lib/log-helper"; +import type { ServerOptions } from "."; type Logger = { log: (...args: unknown[]) => void; @@ -57,40 +33,38 @@ function createLogger(verboseLogs = false): Logger { }, }; } + /** * Configuration for the MCP handler. - * @property redisUrl - The URL of the Redis instance to use for the MCP handler. - * @property streamableHttpEndpoint - The endpoint to use for the streamable HTTP transport. - * @property sseEndpoint - The endpoint to use for the SSE transport. - * @property verboseLogs - If true, enables console logging. */ export type Config = { /** - * The URL of the Redis instance to use for the MCP handler. - * @default process.env.REDIS_URL || process.env.KV_URL + * @deprecated Redis is no longer used. The legacy HTTP+SSE transport + * (protocol 2024-11-05) has been removed; both the 2026-07-28 protocol and + * 2025-era Streamable HTTP clients are served statelessly without Redis. */ redisUrl?: string; /** * The endpoint to use for the streamable HTTP transport. - * @deprecated Use `set basePath` instead. + * @deprecated Use `basePath` instead. * @default "/mcp" */ streamableHttpEndpoint?: string; /** - * The endpoint to use for the SSE transport. - * @deprecated Use `set basePath` instead. + * @deprecated The legacy HTTP+SSE transport has been removed. Requests to + * this endpoint receive `410 Gone`. * @default "/sse" */ sseEndpoint?: string; /** - * The endpoint to use for the SSE messages transport. - * @deprecated Use `set basePath` instead. + * @deprecated The legacy HTTP+SSE transport has been removed. Requests to + * this endpoint receive `410 Gone`. * @default "/message" */ sseMessageEndpoint?: string; /** - * The maximum duration of an MCP request in seconds. - * @default 60 + * @deprecated No longer used. Requests are served per-invocation; there is + * no long-lived SSE session to bound. */ maxDuration?: number; /** @@ -104,8 +78,8 @@ export type Config = { * For example, if basePath is "/", that means your routing is: * /app/[transport]/route.ts and then: * - streamableHttpEndpoint will be "/mcp" - * - sseEndpoint will be "/sse" - * - sseMessageEndpoint will be "/message" + * - sseEndpoint will be "/sse" (removed transport, answered with 410) + * - sseMessageEndpoint will be "/message" (removed transport, answered with 410) * @default "" */ basePath?: string; @@ -116,15 +90,17 @@ export type Config = { onEvent?: (event: McpEvent) => void; /** - * If true, disables the SSE endpoint. - * As of 2025-03-26, SSE is not supported by the MCP spec. - * https://modelcontextprotocol.io/specification/2025-03-26/basic/transports + * If true, the removed SSE endpoints respond `404 Not Found` instead of + * `410 Gone`. * @default false */ disableSse?: boolean; /** * sessionIdGenerator for the streamable HTTP transport + * @deprecated Sessions no longer exist: the 2026-07-28 protocol is + * stateless by design and 2025-era requests are served through the SDK's + * stateless legacy fallback. */ sessionIdGenerator?: undefined; }; @@ -148,6 +124,7 @@ function deriveEndpointsFromBasePath(basePath: string): { sseMessageEndpoint: `${normalizedBasePath}/message`, }; } + /** * Calculates the endpoints for the MCP handler. * @param config - The configuration for the MCP handler. @@ -178,78 +155,21 @@ export function calculateEndpoints({ }; } -let redisPublisher: ReturnType; -let redis: ReturnType; - -// WeakMap to track server metadata without preventing GC -const serverMetadata = new WeakMap(); - -// Periodic cleanup interval -let cleanupInterval: NodeJS.Timeout | null = null; - -async function initializeRedis({ - redisUrl, - logger, -}: { - redisUrl?: string; - logger: Logger; -}) { - if (redis && redisPublisher) { - return { redis, redisPublisher }; - } - - if (!redisUrl) { - throw new Error("redisUrl is required"); - } - - redis = createClient({ - url: redisUrl, - }); - redisPublisher = createClient({ - url: redisUrl, - }); - redis.on("error", (err) => { - logger.error("Redis error", err); - }); - redisPublisher.on("error", (err) => { - logger.error("Redis error", err); - }); - - await Promise.all([redis.connect(), redisPublisher.connect()]); - - return { redis, redisPublisher }; -} - export function initializeMcpApiHandler( initializeServer: | ((server: McpServer) => Promise) | ((server: McpServer) => void), serverOptions: ServerOptions = {}, - config: Config = { - redisUrl: process.env.REDIS_URL || process.env.KV_URL, - streamableHttpEndpoint: "/mcp", - sseEndpoint: "/sse", - sseMessageEndpoint: "/message", - basePath: "", - maxDuration: 60, - verboseLogs: false, - disableSse: false, - } -) { + config: Config = {} +): (req: Request) => Promise { const { - redisUrl, basePath, streamableHttpEndpoint: explicitStreamableHttpEndpoint, sseEndpoint: explicitSseEndpoint, sseMessageEndpoint: explicitSseMessageEndpoint, - maxDuration, verboseLogs, disableSse, - sessionIdGenerator, + onEvent, } = config; const { @@ -260,7 +180,6 @@ export function initializeMcpApiHandler( ...mcpServerOptions } = serverOptions; - // If basePath is provided, derive endpoints from it const { streamableHttpEndpoint, sseEndpoint, sseMessageEndpoint } = calculateEndpoints({ basePath, @@ -271,645 +190,117 @@ export function initializeMcpApiHandler( const logger = createLogger(verboseLogs); - let servers: McpServer[] = []; + const emitError = (error: Error) => { + logger.error("MCP handler error:", error); + onEvent?.( + createEvent({ + type: "ERROR", + error, + source: "request", + severity: "error", + }) + ); + }; - // Note: In SDK 1.26.0+, stateless transports cannot be reused across requests. - // We create a fresh transport and server per POST request. - - // Start periodic cleanup if not already running - if (!cleanupInterval) { - cleanupInterval = setInterval(() => { - const now = Date.now(); - const staleThreshold = 5 * 60 * 1000; // 5 minutes - - servers = servers.filter(server => { - const metadata = serverMetadata.get(server); - if (!metadata) { - // No metadata means the server is orphaned - logger.log("Removing orphaned server without metadata"); - try { - if (server?.server) { - server.server.close(); - } - } catch (error) { - logger.error("Error closing orphaned server:", error); - } - return false; - } - - const age = now - metadata.createdAt.getTime(); - if (age > staleThreshold) { - logger.log(`Removing stale server (session ${metadata.sessionId}, age: ${age}ms)`); - try { - if (server?.server) { - server.server.close(); - } - if (metadata.transport?.close) { - metadata.transport.close(); - } - } catch (error) { - logger.error("Error closing stale server:", error); - } - serverMetadata.delete(server); - return false; - } - - return true; - }); - }, 30 * 1000); // Run every 30 seconds - } + // The SDK handler serves the 2026-07-28 protocol (stateless, per-request + // envelope, server/discover) and falls back to stateless serving for + // 2025-era Streamable HTTP clients. A fresh McpServer is constructed per + // request via the factory. + const sdkHandler = createSdkMcpHandler( + async () => { + const server = new McpServer(serverInfo, mcpServerOptions); + await initializeServer(server); + return server; + }, + { + legacy: "stateless", + onerror: emitError, + } + ); - return async function mcpApiHandler(req: Request, res: ServerResponse) { + return async function mcpApiHandler(req: Request): Promise { const url = new URL(req.url || "", "https://example.com"); - if (url.pathname === streamableHttpEndpoint) { - if (req.method === "GET") { - logger.log("Received GET MCP request"); - res.writeHead(405).end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Method not allowed.", - }, - id: null, - }) - ); - return; - } - if (req.method === "DELETE") { - logger.log("Received DELETE MCP request"); - res.writeHead(405).end( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Method not allowed.", - }, - id: null, - }) - ); - return; - } - if (req.method === "POST") { - const eventRes = new EventEmittingResponse( - createFakeIncomingMessage(), - config.onEvent - ); - - // Parse the request body - let bodyContent: BodyType; - const contentType = req.headers.get("content-type") || ""; - if (contentType.includes("application/json")) { - bodyContent = await req.json(); - } else { - bodyContent = await req.text(); - } - - // In SDK 1.26.0+, stateless transports cannot be reused across requests. - // Create a fresh transport and server per POST request and use the - // WebStandard transport directly since we already have a Web Request. - const transport = new WebStandardStreamableHTTPServerTransport({ - sessionIdGenerator: sessionIdGenerator, - }); - const server = new McpServer(serverInfo, mcpServerOptions); - await initializeServer(server); - await server.connect(transport); + if (url.pathname === streamableHttpEndpoint) { + let method: string | undefined; + let parsedBody: unknown; + const started = Date.now(); + if ( + req.method === "POST" && + (req.headers.get("content-type") || "").includes("application/json") + ) { try { - // Build a new Request with the already-parsed body so the transport - // doesn't try to consume the (already-read) body stream again. - const webReq = new Request(req.url, { - method: req.method, - headers: req.headers, - body: JSON.stringify(bodyContent), - }); - // Propagate auth info for downstream tool handlers - (webReq as any).auth = req.auth; - - const webResp = await transport.handleRequest(webReq, { - authInfo: req.auth, - }); - - // Write the response back through the ServerResponse adapter - res.writeHead(webResp.status, Object.fromEntries(webResp.headers)); - if (webResp.body) { - const reader = webResp.body.getReader(); - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - res.write(value); - } - } finally { - reader.releaseLock(); - } - } - res.end(); - + parsedBody = await req.clone().json(); if ( - typeof bodyContent === "object" && - bodyContent && - "method" in bodyContent + typeof parsedBody === "object" && + parsedBody !== null && + "method" in parsedBody ) { - eventRes.requestCompleted( - bodyContent.method as string, - bodyContent + method = String( + (parsedBody as { method: unknown }).method ); - } - } catch (error) { - if ( - typeof bodyContent === "object" && - bodyContent && - "method" in bodyContent - ) { - eventRes.requestCompleted( - bodyContent.method as string, - undefined, - error instanceof Error ? error : String(error) + onEvent?.( + createEvent({ + type: "REQUEST_RECEIVED", + method, + parameters: parsedBody, + status: "success", + }) ); } - throw error; + } catch { + // Malformed JSON is rejected by the SDK handler below. } } - } else if (url.pathname === sseEndpoint) { - if (disableSse) { - res.statusCode = 404; - res.end("Not found"); - return; - } - - // Check HTTP method - only allow GET for SSE connections - if (req.method !== "GET") { - logger.log(`Rejected SSE connection with method ${req.method}`); - res - .writeHead(405, { "Content-Type": "text/plain" }) - .end("Method Not Allowed"); - return; - } - - // Check that Accept header supports event-stream - const acceptHeader = - req.headers.get("accept") || req.headers.get("Accept"); - if ( - acceptHeader && - !acceptHeader.includes("text/event-stream") && - !acceptHeader.includes("*/*") && - !acceptHeader.includes("text/*") - ) { - logger.log( - `Rejected SSE connection with incompatible Accept header: ${acceptHeader}` - ); - res - .writeHead(406, { "Content-Type": "text/plain" }) - .end("Not Acceptable"); - return; - } - - const { redis, redisPublisher } = await initializeRedis({ - redisUrl, - logger, - }); - logger.log("Got new SSE connection"); - assert(sseMessageEndpoint, "sseMessageEndpoint is required"); - const transport = new SSEServerTransport(sseMessageEndpoint, res); - const sessionId = transport.sessionId; - - const eventRes = new EventEmittingResponse( - createFakeIncomingMessage(), - config.onEvent, - sessionId - ); - eventRes.startSession("SSE", { - userAgent: req.headers.get("user-agent") ?? undefined, - ip: - req.headers.get("x-forwarded-for") ?? - req.headers.get("x-real-ip") ?? - undefined, - }); - const server = new McpServer(serverInfo, serverOptions); - - // Track cleanup state to prevent double cleanup - let isCleanedUp = false; - let interval: NodeJS.Timeout | null = null; - let timeout: NodeJS.Timeout | null = null; - let abortHandler: (() => void) | null = null; - let handleMessage: ((message: string) => Promise) | null = null; - let logs: { type: LogLevel; messages: string[]; }[] = []; - - // Comprehensive cleanup function - const cleanup = async (reason: string) => { - if (isCleanedUp) return; - isCleanedUp = true; - - logger.log(`Cleaning up SSE connection: ${reason}`); - - // Clear timers - if (timeout) { - clearTimeout(timeout); - timeout = null; - } - if (interval) { - clearInterval(interval); - interval = null; - } - - // Remove abort event listener - if (abortHandler) { - req.signal.removeEventListener("abort", abortHandler); - abortHandler = null; - } - - // Unsubscribe from Redis - if (handleMessage) { - try { - await redis.unsubscribe(`requests:${sessionId}`, handleMessage); - logger.log(`Unsubscribed from requests:${sessionId}`); - } catch (error) { - logger.error("Error unsubscribing from Redis:", error); - } - } - - // Close server and transport - try { - if (server?.server) { - await server.server.close(); - } - if (transport?.close) { - await transport.close(); - } - } catch (error) { - logger.error("Error closing server/transport:", error); - } - - // Remove server from array and WeakMap - servers = servers.filter((s) => s !== server); - serverMetadata.delete(server); - - // End session event - eventRes.endSession("SSE"); - - // Clear logs array to free memory - logs = []; - - // End response if not already ended - if (!res.headersSent) { - res.statusCode = 200; - res.end(); - } - }; - try { - await initializeServer(server); - servers.push(server); - - // Store metadata in WeakMap - serverMetadata.set(server, { - sessionId, - createdAt: new Date(), - transport + const response = await sdkHandler.fetch(req, { + authInfo: req.auth, + parsedBody, }); - server.server.onclose = () => { - cleanup("server closed"); - }; - - // eslint-disable-next-line no-inner-declarations - function logInContext(severity: LogLevel, ...messages: string[]) { - logs.push({ - type: severity, - messages, - }); - } - - // Handles messages originally received via /message - handleMessage = async (message: string) => { - logger.log("Received message from Redis", message); - logInContext("log", "Received message from Redis", message); - const request = JSON.parse(message) as SerializedRequest; - - // Make in IncomingMessage object because that is what the SDK expects. - // Pass auth from the serialized request to preserve the caller's auth context - const req = createFakeIncomingMessage({ - method: request.method, - url: request.url, - headers: request.headers, - body: request.body, - auth: request.auth, - }); - - const syntheticRes = new EventEmittingResponse( - req, - config.onEvent, - sessionId - ); - let status = 100; - let body = ""; - syntheticRes.writeHead = (statusCode: number) => { - status = statusCode; - return syntheticRes; - }; - syntheticRes.end = (b: unknown) => { - body = b as string; - return syntheticRes; - }; - - try { - await transport.handlePostMessage(req, syntheticRes); - - // If it was a function call, complete it - if ( - typeof request.body === "object" && - request.body && - "method" in request.body - ) { - try { - const result = JSON.parse(body); - eventRes.requestCompleted(request.body.method as string, result); - } catch { - eventRes.requestCompleted(request.body.method as string, body); - } - } - } catch (error) { - eventRes.error( - error instanceof Error ? error : String(error), - "Error handling SSE message", - "session" - ); - throw error; - } - - await redisPublisher.publish( - `responses:${sessionId}:${request.requestId}`, - JSON.stringify({ - status, - body, + if (method) { + onEvent?.( + createEvent({ + type: "REQUEST_COMPLETED", + method, + duration: Date.now() - started, + status: response.ok ? "success" : "error", }) ); - - if (status >= 200 && status < 300) { - logInContext( - "log", - `Request ${sessionId}:${request.requestId} succeeded: ${body}` - ); - } else { - logInContext( - "error", - `Message for ${sessionId}:${request.requestId} failed with status ${status}: ${body}` - ); - eventRes.error( - `Request failed with status ${status}`, - body, - "session" - ); - } - }; - - interval = setInterval(() => { - for (const log of logs) { - logger[log.type](...log.messages); - } - logs = []; - }, 100); - - await redis.subscribe(`requests:${sessionId}`, handleMessage); - logger.log(`Subscribed to requests:${sessionId}`); - - let resolveTimeout: (value: unknown) => void; - const waitPromise = new Promise((resolve) => { - resolveTimeout = resolve; - timeout = setTimeout(() => { - resolve("max duration reached"); - }, (maxDuration ?? 60) * 1000); - }); - - abortHandler = () => resolveTimeout("client hang up"); - req.signal.addEventListener("abort", abortHandler); - - // Handle response close event - res.on("close", () => { - cleanup("response closed"); - }); - - // Handle response error event - res.on("error", (error) => { - logger.error("Response error:", error); - cleanup("response error"); - }); - - await server.connect(transport); - const closeReason = await waitPromise; - logger.log(closeReason); - await cleanup(String(closeReason)); + } + return response; } catch (error) { - logger.error("Error in SSE handler:", error); - await cleanup("error during setup"); + emitError(error instanceof Error ? error : new Error(String(error))); throw error; } - } else if (url.pathname === sseMessageEndpoint) { - if (disableSse) { - res.statusCode = 404; - res.end("Not found"); - return; - } - - const { redis, redisPublisher } = await initializeRedis({ - redisUrl, - logger, - }); - logger.log("Received message"); - - const body = await req.text(); - let parsedBody: BodyType; - try { - parsedBody = JSON.parse(body); - } catch (e) { - parsedBody = body; - } + } - const sessionId = url.searchParams.get("sessionId") || ""; - if (!sessionId) { - res.statusCode = 400; - res.end("No sessionId provided"); - return; + if (url.pathname === sseEndpoint || url.pathname === sseMessageEndpoint) { + if (disableSse) { + return new Response("Not found", { status: 404 }); } - const requestId = crypto.randomUUID(); - const serializedRequest: SerializedRequest = { - requestId, - url: req.url || "", - method: req.method || "", - body: parsedBody, - headers: Object.fromEntries(req.headers.entries()), - auth: req.auth, - }; - - // Declare timeout and response handling state before subscription - let timeout: NodeJS.Timeout | null = null; - let hasResponded = false; - let isCleanedUp = false; - - // Cleanup function to ensure all resources are freed - const cleanup = async () => { - if (isCleanedUp) return; - isCleanedUp = true; - - if (timeout) { - clearTimeout(timeout); - timeout = null; - } - - try { - await redis.unsubscribe(`responses:${sessionId}:${requestId}`); - } catch (error) { - logger.error("Error unsubscribing from Redis response channel:", error); - } - }; - - // Safe response handler to prevent double res.end() - const sendResponse = async (status: number, body: string) => { - if (!hasResponded) { - hasResponded = true; - res.statusCode = status; - res.end(body); - await cleanup(); - } - }; - - // Response handler - const handleResponse = async (message: string) => { - try { - const response = JSON.parse(message) as { - status: number; - body: string; - }; - await sendResponse(response.status, response.body); - } catch (error) { - logger.error("Failed to parse response message:", error); - await sendResponse(500, "Internal server error"); - } - }; - - try { - // Handles responses from the /sse endpoint. - await redis.subscribe( - `responses:${sessionId}:${requestId}`, - handleResponse - ); - - // Queue the request in Redis so that a subscriber can pick it up. - // One queue per session. - await redisPublisher.publish( - `requests:${sessionId}`, - JSON.stringify(serializedRequest) - ); - logger.log(`Published requests:${sessionId}`, serializedRequest); - - // Set timeout after subscription is established - timeout = setTimeout(async () => { - await sendResponse(408, "Request timed out"); - }, 10 * 1000); - - // Handle response close event - res.on("close", async () => { - if (!hasResponded) { - hasResponded = true; - await cleanup(); - } - }); - - // Handle response error event - res.on("error", async (error) => { - logger.error("Response error in message handler:", error); - if (!hasResponded) { - hasResponded = true; - await cleanup(); - } - }); - } catch (error) { - logger.error("Error in message handler:", error); - await cleanup(); - if (!hasResponded) { - res.statusCode = 500; - res.end("Internal server error"); + logger.log( + `Received request for removed SSE transport endpoint: ${url.pathname}` + ); + return new Response( + JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32000, + message: + "The HTTP+SSE transport (protocol 2024-11-05) is no longer supported. Connect using Streamable HTTP.", + }, + id: null, + }), + { + status: 410, + headers: { "Content-Type": "application/json" }, } - } - } else { - res.statusCode = 404; - res.end("Not found"); - } - }; -} - -// Define the options interface -interface FakeIncomingMessageOptions { - method?: string; - url?: string; - headers?: IncomingHttpHeaders; - body?: BodyType; - socket?: Socket; - auth?: AuthInfo; -} - -// Create a fake IncomingMessage -function createFakeIncomingMessage( - options: FakeIncomingMessageOptions = {} -): IncomingMessage & { auth?: AuthInfo } { - const { - method = "GET", - url = "/", - headers = {}, - body = null, - socket = new Socket(), - } = options; - - // Create a readable stream that will be used as the base for IncomingMessage - const readable = new Readable(); - readable._read = (): void => {}; // Required implementation - - // Add the body content if provided - if (body) { - if (typeof body === "string") { - readable.push(body); - } else if (Buffer.isBuffer(body)) { - readable.push(body); - } else { - // Ensure proper JSON-RPC format - const bodyString = JSON.stringify(body); - readable.push(bodyString); + ); } - readable.push(null); // Signal the end of the stream - } else { - readable.push(null); // Always end the stream even if no body - } - // Create the IncomingMessage instance - const req = new IncomingMessage(socket) as IncomingMessage & { - auth?: AuthInfo; + return new Response("Not found", { status: 404 }); }; - - // Set the properties - req.method = method; - req.url = url; - req.headers = headers; - req.rawHeaders = Object.entries(headers).flatMap(([key, value]) => - Array.isArray(value) - ? value.flatMap(v => [key, v]) - : [key, value ?? ""] - ); - - const auth = options.auth || getAuthContext(); - if (auth) { - // See https://github.com/modelcontextprotocol/typescript-sdk/blob/590d4841373fc4eb86ecc9079834353a98cb84a3/src/server/auth/middleware/bearerAuth.ts#L71 for more info. - (req as { auth?: AuthInfo }).auth = auth; - } - - // Copy over the stream methods - req.push = readable.push.bind(readable); - req.read = readable.read.bind(readable); - // @ts-expect-error - req.on = readable.on.bind(readable); - req.pipe = readable.pipe.bind(readable); - - return req; } diff --git a/src/handler/server-response-adapter.ts b/src/handler/server-response-adapter.ts deleted file mode 100644 index b817e21..0000000 --- a/src/handler/server-response-adapter.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { EventEmitter } from 'node:events'; -import type { ServerResponse } from 'node:http'; - -type WriteheadArgs = { - statusCode: number; - headers?: Record; -}; - -// biome-ignore lint/suspicious/noExplicitAny: Not deterministic -export type BodyType = string | Buffer | Record | null; - -type EventListener = (...args: unknown[]) => void; - -/** - * Anthropic's MCP API requires a server response object. This function - * creates a fake server response object that can be used to pass to the MCP API. - */ -export function createServerResponseAdapter( - signal: AbortSignal, - fn: (re: ServerResponse) => Promise | void -): Promise { - let writeHeadResolver: (v: WriteheadArgs) => void; - const writeHeadPromise = new Promise(resolve => { - writeHeadResolver = resolve; - }); - - return new Promise(resolve => { - let controller: ReadableStreamController | undefined; - let shouldClose = false; - let wroteHead = false; - let statusCode = 200; - let headers: Record | undefined; - - const writeHead = (code: number, headersArg?: Record) => { - if (typeof headersArg === 'string') { - throw new Error('Status message of writeHead not supported'); - } - statusCode = code; - headers = headersArg; - wroteHead = true; - writeHeadResolver({ - statusCode, - headers, - }); - return fakeServerResponse; - }; - - const bufferedData: Uint8Array[] = []; - - const write = ( - chunk: Buffer | string | Uint8Array, - encoding?: BufferEncoding - ): boolean => { - if (encoding) { - throw new Error('Encoding not supported'); - } - if (chunk instanceof Buffer) { - throw new Error('Buffer not supported'); - } - - // SDK 1.25+ uses Hono which sends Uint8Array (already encoded) - // SDK 1.24- sends strings that need encoding - let data: Uint8Array; - if (chunk instanceof Uint8Array) { - data = chunk; - } else if (typeof chunk === 'string') { - data = new TextEncoder().encode(chunk); - } else { - throw new Error('Unexpected chunk type: ' + typeof chunk); - } - - if (!wroteHead) { - writeHead(statusCode, headers); - } - if (!controller) { - bufferedData.push(data); - return true; - } - controller.enqueue(data); - return true; - }; - - const eventEmitter = new EventEmitter(); - - const fakeServerResponse = { - writeHead, - write, - end: (data?: Buffer | string) => { - if (data) { - write(data); - } - - if (!controller) { - shouldClose = true; - return fakeServerResponse; - } - try { - controller.close(); - } catch { - /* May be closed on tcp layer */ - } - return fakeServerResponse; - }, - on: (event: string, listener: EventListener) => { - eventEmitter.on(event, listener); - return fakeServerResponse; - }, - get statusCode() { - return statusCode; - }, - set statusCode(code: number) { - statusCode = code; - - // If the status code is set after writeHead, we need to call - // writeHead again to update the status code. - if (wroteHead) { - writeHeadResolver({ - statusCode, - headers, - }); - } - }, - }; - - signal.addEventListener('abort', () => { - eventEmitter.emit('close'); - }); - - void fn(fakeServerResponse as ServerResponse); - - void (async () => { - const head = await writeHeadPromise; - - const response = new Response( - new ReadableStream({ - start(c) { - controller = c; - for (const chunk of bufferedData) { - controller.enqueue(chunk); - } - if (shouldClose) { - controller.close(); - } - }, - }), - { - status: head.statusCode, - headers: head.headers, - } - ); - - resolve(response); - })(); - }); -} diff --git a/src/lib/event-emitter.ts b/src/lib/event-emitter.ts deleted file mode 100644 index bf18c0b..0000000 --- a/src/lib/event-emitter.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { ServerResponse, type IncomingMessage } from "node:http"; -import { - type McpErrorEvent, - type McpEvent, - type McpRequestEvent, - type McpSessionEvent, - createEvent, -} from "./log-helper"; - -export class EventEmittingResponse extends ServerResponse { - private onEvent?: (event: McpEvent) => void; - private sessionId?: string; - private requestId: string; - private startTime: number; - - constructor( - req: IncomingMessage, - onEvent?: (event: McpEvent) => void, - sessionId?: string - ) { - super(req); - this.onEvent = onEvent; - this.sessionId = sessionId; - this.requestId = crypto.randomUUID(); - this.startTime = Date.now(); - } - - emitEvent(event: Omit) { - if (this.onEvent) { - this.onEvent( - createEvent({ - ...event, - sessionId: this.sessionId, - requestId: this.requestId, - } as Omit) - ); - } - } - - startSession( - transport: "SSE" | "HTTP", - clientInfo?: { userAgent?: string; ip?: string } - ) { - this.emitEvent({ - type: "SESSION_STARTED", - transport, - clientInfo, - } as Omit); - } - - endSession(transport: "SSE" | "HTTP") { - this.emitEvent({ - type: "SESSION_ENDED", - transport, - } as Omit); - } - - requestReceived(method: string, parameters?: unknown) { - this.emitEvent({ - type: "REQUEST_RECEIVED", - method, - parameters, - status: "success", - } as Omit); - } - - requestCompleted(method: string, result?: unknown, error?: Error | string) { - this.emitEvent({ - type: "REQUEST_COMPLETED", - method, - result, - duration: Date.now() - this.startTime, - status: error ? "error" : "success", - } as Omit); - - if (error) { - this.error(error, `Error executing request ${method}`, "request"); - } - } - - error( - error: Error | string, - context?: string, - source: McpErrorEvent["source"] = "system", - severity: McpErrorEvent["severity"] = "error" - ) { - this.emitEvent({ - type: "ERROR", - error, - context, - source, - severity, - } as Omit); - } - - end( - chunk?: unknown, - encoding?: BufferEncoding | (() => void), - cb?: () => void - ): this { - let finalChunk = chunk; - let finalEncoding = encoding; - let finalCallback = cb; - - if (typeof chunk === "function") { - finalCallback = chunk as () => void; - finalChunk = undefined; - finalEncoding = undefined; - } else if (typeof encoding === "function") { - finalCallback = encoding as () => void; - finalEncoding = undefined; - } - - return super.end( - finalChunk as string | Buffer, - finalEncoding as BufferEncoding, - finalCallback - ); - } -} diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index 7fea7a3..32467d1 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -7,8 +7,10 @@ import { type Server, } from "node:http"; import type { AddressInfo } from "node:net"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { + Client, + StreamableHTTPClientTransport, +} from "@modelcontextprotocol/client"; import { createMcpHandler } from "../src/index"; import { withMcpAuth } from "../src/auth/auth-wrapper"; @@ -25,16 +27,15 @@ describe("e2e", () => { "echo", { description: "Echo a message", - inputSchema: { message: z.string() }, + inputSchema: z.object({ message: z.string() }), }, - async ({ message }, extra) => { + async ({ message }, ctx) => { + const token = ctx.http?.authInfo?.token; return { content: [ { type: "text", - text: `Tool echo: ${message}${ - extra.authInfo?.token ? ` for ${extra.authInfo?.token}` : "" - }`, + text: `Tool echo: ${message}${token ? ` for ${token}` : ""}`, }, ], }; @@ -45,9 +46,9 @@ describe("e2e", () => { "greeting", { description: "Generate a greeting message", - argsSchema: { + argsSchema: z.object({ name: z.string().describe("The name of the person to greet"), - }, + }), }, async ({ name }) => { return { @@ -99,10 +100,8 @@ describe("e2e", () => { }, }, { - redisUrl: process.env.KV_URL, basePath: "", verboseLogs: true, - maxDuration: 60, } ); @@ -155,28 +154,21 @@ describe("e2e", () => { expect(capabilities?.tools).toBeDefined(); expect(capabilities?.prompts).toBeDefined(); expect(capabilities?.resources).toBeDefined(); - expect((await client.listTools()).tools).toStrictEqual([ - { - "description": "Echo a message", - "execution": { - "taskSupport": "forbidden", - }, - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "message": { - "type": "string", - }, + const { tools } = await client.listTools(); + expect(tools).toHaveLength(1); + expect(tools[0]).toMatchObject({ + name: "echo", + description: "Echo a message", + inputSchema: { + type: "object", + properties: { + message: { + type: "string", }, - "required": [ - "message", - ], - "type": "object", }, - "name": "echo", + required: ["message"], }, - ]); + }); expect((await client.listPrompts()).prompts).toStrictEqual([ { "arguments": [ @@ -270,9 +262,7 @@ describe("e2e", () => { arguments: { message: "Are you there?", }, - }, - undefined, - {} + } ); expect((result.content as any)[0].text).toEqual( "Tool echo: Are you there?" @@ -306,15 +296,93 @@ describe("e2e", () => { arguments: { message: "Are you there?", }, + } + ); + expect((result.content as any)[0].text).toEqual( + "Tool echo: Are you there? for ACCESS_TOKEN" + ); + }); + + it("should serve the 2026-07-28 protocol to a pinned modern client", async () => { + const modernTransport = new StreamableHTTPClientTransport( + new URL(`${endpoint}/mcp`) + ); + const modernClient = new Client( + { + name: "modern-client", + version: "1.0.0", }, - undefined, - {} + { + capabilities: {}, + versionNegotiation: { mode: { pin: "2026-07-28" } }, + } ); + await modernClient.connect(modernTransport); + + const { tools } = await modernClient.listTools(); + expect(tools).toHaveLength(1); + expect(tools[0].name).toEqual("echo"); + + const result = await modernClient.callTool({ + name: "echo", + arguments: { + message: "Hello from the future", + }, + }); + expect((result.content as any)[0].text).toEqual( + "Tool echo: Hello from the future" + ); + }); + + it("should pass auth info through on the modern protocol path", async () => { + const modernTransport = new StreamableHTTPClientTransport( + new URL(`${endpoint}/mcp`), + { + requestInit: { + headers: { + Authorization: `Bearer ACCESS_TOKEN`, + }, + }, + } + ); + const modernClient = new Client( + { + name: "modern-client", + version: "1.0.0", + }, + { + capabilities: {}, + versionNegotiation: { mode: { pin: "2026-07-28" } }, + } + ); + await modernClient.connect(modernTransport); + const result = await modernClient.callTool({ + name: "echo", + arguments: { + message: "Are you there?", + }, + }); expect((result.content as any)[0].text).toEqual( "Tool echo: Are you there? for ACCESS_TOKEN" ); }); + it("should answer 410 Gone on the removed SSE transport endpoints", async () => { + const sseRes = await fetch(`${endpoint}/sse`, { + headers: { Accept: "text/event-stream" }, + }); + expect(sseRes.status).toEqual(410); + const body = await sseRes.json(); + expect(body.error.message).toContain("no longer supported"); + + const messageRes = await fetch(`${endpoint}/message?sessionId=foo`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }), + }); + expect(messageRes.status).toEqual(410); + }); + it("should return an invalid token error when verifyToken fails", async () => { const authenticatedTransport = new StreamableHTTPClientTransport( new URL(`${endpoint}/mcp`), diff --git a/tsconfig.json b/tsconfig.json index da55d8d..d95ef3e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { - "target": "es2016", - "module": "commonjs", + "target": "es2022", + "module": "esnext", + "moduleResolution": "bundler", "declaration": true, "outDir": "./dist", "esModuleInterop": true, diff --git a/tsup.config.ts b/tsup.config.ts index 4c865a1..3559fd5 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: ['src/index.ts', 'src/next/index.ts', 'src/cli/index.ts'], + entry: ['src/index.ts', 'src/cli/index.ts'], format: ['esm', 'cjs'], dts: true, splitting: true,