From 61c6049aff6b699cd10104f2e431633386f892fb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 11:56:27 +0000 Subject: [PATCH 1/3] feat!: move to MCP SDK v2 and Zod 4 Replace @modelcontextprotocol/sdk with @modelcontextprotocol/server and @modelcontextprotocol/node, and zod ^3.25 with ^4.6.7 (SDK v2 cannot read Zod 3 schemas, so the two move together). - Transports use NodeStreamableHTTPServerTransport; session handling and the per-client bundle model are unchanged. - Tool schemas go through toRegistrableInputSchema(): Zod 4 schemas as is, raw shapes wrapped with toolception's own z.object(), Zod 3 refused with E_VALIDATION, JSON Schema kept as an open z.looseObject({}). - Add multi-client acceptance tests (DYNAMIC, STATIC, permission-based, session context) and a single-copy dependency test. - Fix examples, README, intent nodes and changelog; vite externals follow the new packages; npm audit fix clears the zod high advisory. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01VmUXGSYMuTcM7fquxzRook --- AGENTS.md | 2 + CHANGELOG.md | 15 + README.md | 81 +- examples/custom-endpoints-demo.ts | 2 +- package-lock.json | 1115 +++-------------- package.json | 6 +- src/core/AGENTS.md | 4 +- src/core/DynamicToolManager.ts | 78 +- src/core/ServerOrchestrator.ts | 2 +- src/core/core.types.ts | 2 +- src/http/AGENTS.md | 10 +- src/http/FastifyTransport.ts | 20 +- src/http/http.types.ts | 2 +- src/http/http.utils.ts | 2 +- src/meta/registerMetaTools.ts | 12 +- .../PermissionAwareFastifyTransport.ts | 22 +- src/permissions/permissions.types.ts | 2 +- src/permissions/permissions.utils.ts | 2 +- src/server/createMcpServer.ts | 2 +- src/server/createPermissionBasedMcpServer.ts | 2 +- src/server/server.types.ts | 2 +- src/types/AGENTS.md | 2 +- src/types/index.ts | 7 +- tests/createPermissionBasedMcpServer.test.ts | 31 +- tests/customEndpoints.integration.test.ts | 2 +- tests/dependencies.test.ts | 38 + tests/dynamicToolManager.test.ts | 88 +- tests/e2e/dynamicMode.e2e.test.ts | 15 +- tests/e2e/jsonSchemaTools.e2e.test.ts | 5 +- tests/e2e/multiClient.e2e.test.ts | 349 ++++++ tests/e2e/permissionBased.e2e.test.ts | 5 +- tests/e2e/staticMode.e2e.test.ts | 5 +- tests/originValidation.test.ts | 5 +- tests/permissionAwareFastifyTransport.test.ts | 5 +- tests/smoke-e2e/client-demo.ts | 3 +- .../permission-config-client-demo.ts | 3 +- .../permission-config-server-demo.ts | 5 +- .../permission-header-client-demo.ts | 3 +- .../permission-header-server-demo.ts | 5 +- tests/smoke-e2e/server-demo.ts | 5 +- vite.config.ts | 2 +- 41 files changed, 823 insertions(+), 1145 deletions(-) create mode 100644 tests/dependencies.test.ts create mode 100644 tests/e2e/multiClient.e2e.test.ts diff --git a/AGENTS.md b/AGENTS.md index da766ee..6e0bc39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,6 +50,8 @@ These are cross-cutting; see leaf Intent Nodes for module-specific invariants. 3. **Fail-secure** — Invalid inputs return empty objects/arrays, not errors 4. **Module loader errors are silent, registration failures are not** — A throwing loader is logged and the toolset activates with fewer tools. A failure while registering a startup toolset's tools (for example a duplicate tool name) makes `isReady()` return `false`: STATIC `createMcpServer` throws at startup and its transport answers the client's `initialize` with a 500 / `-32603` and drops the bundle. `createPermissionBasedMcpServer` is the exception: it enables toolsets in its bundle factory and continues on partial failure 5. **Disable unregisters tools** — `disableToolset` removes the toolset's tools from the MCP server via the SDK's `RegisteredTool.remove()`; see `src/core/AGENTS.md` +6. **Clients never share toolset state** — Every `mcp-client-id` (plus session-context cache key) gets its own `McpServer`, orchestrator and `NodeStreamableHTTPServerTransport`. Enabling or disabling a toolset, `tools/list_changed` notifications and session context (tokens) of one client are never visible to another. `tests/e2e/multiClient.e2e.test.ts` is the guard; keep it green on every SDK or transport change +7. **MCP SDK v2 and Zod 4 only** — Built on `@modelcontextprotocol/server` and `@modelcontextprotocol/node`. SDK v2 cannot read Zod 3 schemas, so tool and endpoint schemas must be Zod 4. There must be one installed copy of `zod` and of `@modelcontextprotocol/server` (`tests/dependencies.test.ts`) ## Module Index diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c5ac6..b8dfb25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to `toolception` are listed here. Versions follow [semver](h Releases before 0.7.0 are described in [GitHub Releases](https://github.com/code-rabi/toolception/releases). +## [Unreleased] + +### Breaking + +- Toolception now runs on MCP TypeScript SDK v2 and Zod 4. The `@modelcontextprotocol/sdk` dependency is replaced by `@modelcontextprotocol/server` and `@modelcontextprotocol/node`, and `zod` moves from ^3.25 to ^4.6.7. SDK v2 does not read Zod 3 schemas, so the two cannot be upgraded separately. + - **Migration:** change your imports to `@modelcontextprotocol/server` (`McpServer`) and `@modelcontextprotocol/client` (`Client`, `StreamableHTTPClientTransport`); install `zod` ^4.6.7; and write tool `inputSchema` values and `defineEndpoint` schemas with Zod 4. `z.record()` needs two arguments in Zod 4. Pass server capabilities as the second `McpServer` argument (`new McpServer(info, { capabilities })`); `capabilities` inside the first argument is rejected by the types. + - A tool whose `inputSchema` is a Zod 3 schema (or a raw shape holding one) now fails to enable with an `E_VALIDATION` error that names the tool. + - `createServer` factories must return a v2 `McpServer`; the removed `server.tool()`, `server.prompt()` and `server.resource()` are replaced by `registerTool`, `registerPrompt` and `registerResource`. +- Calling a tool the server no longer lists (for example a disabled toolset's tool) is now answered with a JSON-RPC error ("Tool ... not found") instead of a result with `isError: true`. This is SDK v2 behaviour. +- `FastifyTransport` and `PermissionAwareFastifyTransport` use `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`. Session handling, the per-client `mcp-client-id` model and the HTTP endpoints are unchanged: each client still gets its own server, transport and enabled/disabled toolset state. + +### Fixed + +- Dependency advisories: Zod 4.6.7 clears the high-severity `zod` finding (GHSA-mrhx-c7xc-m3w5); `body-parser`, `qs` and `ip-address` leave the tree with the v1 SDK's Express dependencies; `fastify` 5.12.8 and `fast-uri` 3.1.9 come from a lockfile refresh. Only the dev-only `minimatch` chain through `vite-plugin-dts` 4 remains, and it does not ship in the package. + ## [0.8.0] ### Breaking diff --git a/README.md b/README.md index f116c67..9013a5d 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,11 @@ Toolception addresses this by grouping tools into toolsets and letting you expos ### Step 1: Install ```bash -npm i toolception +npm i toolception @modelcontextprotocol/server zod ``` +Toolception 0.9 is built on MCP TypeScript SDK v2 (`@modelcontextprotocol/server`) and needs Zod 4 (`zod` ^4.6.7); Zod 3 schemas are rejected. Install the same versions toolception depends on, so your app and toolception share one copy of each. + ### Step 2: Import Toolception ```ts @@ -98,7 +100,7 @@ const catalog = { ### Step 4: Define a tool -`inputSchema` should be a Zod raw shape (or a Zod object schema), the same form `McpServer.registerTool` takes. A plain JSON Schema object still works, but clients see an open object schema and arguments reach the handler unvalidated. +`inputSchema` should be a Zod 4 raw shape (or a Zod 4 object schema); toolception wraps a raw shape with `z.object()` before it calls `McpServer.registerTool`. A Zod 3 schema fails to enable its toolset with an `E_VALIDATION` error. A plain JSON Schema object still works, but clients see an open object schema and arguments reach the handler unvalidated. ```ts import { z } from "zod"; @@ -140,15 +142,14 @@ const configSchema = { ### Step 7: Create the MCP SDK server and start Toolception ```ts -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; // You own the SDK server; pass a factory into Toolception (required in DYNAMIC mode) const createServer = () => new McpServer({ name: "my-mcp-server", version: "0.0.0", - capabilities: { tools: { listChanged: true } }, - }); + }, { capabilities: { tools: { listChanged: true } } }); const { start, close } = await createMcpServer({ catalog, @@ -179,7 +180,7 @@ process.on("SIGTERM", async () => { Enable some or ALL toolsets at bootstrap. In STATIC mode, a single server instance is created and reused for all clients: ```ts -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; const staticCatalog = { search: { name: "Search", description: "Search tools", modules: ["search"] }, @@ -195,8 +196,7 @@ createMcpServer({ new McpServer({ name: "static-1", version: "0.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), }); // Load ALL toolsets @@ -208,8 +208,7 @@ createMcpServer({ new McpServer({ name: "static-2", version: "0.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), }); ``` @@ -220,14 +219,16 @@ Use `createPermissionBasedMcpServer` when you need to enforce client-specific to ### Step 1: Install ```bash -npm i toolception +npm i toolception @modelcontextprotocol/server zod ``` +Toolception 0.9 is built on MCP TypeScript SDK v2 (`@modelcontextprotocol/server`) and needs Zod 4 (`zod` ^4.6.7); Zod 3 schemas are rejected. Install the same versions toolception depends on, so your app and toolception share one copy of each. + ### Step 2: Import Toolception ```ts import { createPermissionBasedMcpServer } from "toolception"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; ``` ### Step 3: Define a toolset catalog @@ -309,8 +310,7 @@ const createServer = () => new McpServer({ name: "permission-header-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }); + }, { capabilities: { tools: { listChanged: false } } }); const { start, close } = await createPermissionBasedMcpServer({ catalog, @@ -333,8 +333,7 @@ const createServer = () => new McpServer({ name: "permission-config-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }); + }, { capabilities: { tools: { listChanged: false } } }); const { start, close } = await createPermissionBasedMcpServer({ catalog, @@ -361,8 +360,7 @@ const createServer = () => new McpServer({ name: "permission-resolver-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }); + }, { capabilities: { tools: { listChanged: false } } }); const { start, close } = await createPermissionBasedMcpServer({ catalog, @@ -410,14 +408,13 @@ Use header-based permissions when you have an authentication gateway or proxy th ```ts import { createPermissionBasedMcpServer } from "toolception"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; const createServer = () => new McpServer({ name: "permission-header-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }); + }, { capabilities: { tools: { listChanged: false } } }); const { start, close } = await createPermissionBasedMcpServer({ catalog: { @@ -464,14 +461,13 @@ Use a static map when you have a fixed set of clients with known permissions. Th ```ts import { createPermissionBasedMcpServer } from "toolception"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; const createServer = () => new McpServer({ name: "permission-config-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }); + }, { capabilities: { tools: { listChanged: false } } }); const { start, close } = await createPermissionBasedMcpServer({ catalog: { @@ -522,14 +518,13 @@ Use a resolver function when you need custom logic to determine permissions, suc ```ts import { createPermissionBasedMcpServer } from "toolception"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; const createServer = () => new McpServer({ name: "permission-resolver-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }); + }, { capabilities: { tools: { listChanged: false } } }); const { start, close } = await createPermissionBasedMcpServer({ catalog: { @@ -723,7 +718,7 @@ Use the `sessionContext` option to enable per-client context values extracted fr ```ts import { createMcpServer } from "toolception"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; const { start } = await createMcpServer({ catalog: { /* ... */ }, @@ -741,8 +736,7 @@ const { start } = await createMcpServer({ createServer: () => new McpServer({ name: "my-server", version: "1.0.0", - capabilities: { tools: { listChanged: true } }, - }), + }, { capabilities: { tools: { listChanged: true } } }), http: { port: 3000 }, }); @@ -1105,8 +1099,7 @@ Note: In STATIC mode, `enable_toolset`, `disable_toolset`, `list_toolsets`, and When connecting to a permission-based server with header-based permissions, include the `mcp-toolset-permissions` header with a comma-separated list of toolsets: ```ts -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; const clientId = "my-client-id"; const allowedToolsets = ["user", "reports"]; // determined by your auth system @@ -1140,8 +1133,7 @@ await client.close(); When connecting to a permission-based server with config-based permissions, only provide the `mcp-client-id` header. The server looks up permissions internally: ```ts -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; const clientId = "admin-client-id"; // matches server's staticMap or resolver @@ -1178,8 +1170,7 @@ await client.close(); Examples (official MCP client) ```ts -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; // Create a stable client id (persist it for reuse across runs) const clientId = "my-stable-client-id"; // e.g., from disk/env @@ -1215,8 +1206,7 @@ await client.close(); Examples (official MCP client) ```ts -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; const clientId = "my-stable-client-id"; const transport = new StreamableHTTPClientTransport( @@ -1382,7 +1372,7 @@ Create a server where each tenant has access to their own toolsets plus shared t ```ts import { createPermissionBasedMcpServer } from "toolception"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; const { start, close } = await createPermissionBasedMcpServer({ catalog: { @@ -1431,8 +1421,7 @@ const { start, close } = await createPermissionBasedMcpServer({ new McpServer({ name: "multi-tenant-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), }); await start(); @@ -1444,7 +1433,7 @@ Integrate with an external authentication system by pre-loading permissions: ```ts import { createPermissionBasedMcpServer } from "toolception"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; // Pre-load permissions from your auth system // This should be done before server creation and cached @@ -1486,8 +1475,7 @@ const { start, close } = await createPermissionBasedMcpServer({ new McpServer({ name: "auth-integrated-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), }); await start(); @@ -1499,7 +1487,7 @@ Implement role-based access control with predefined role-to-toolset mappings: ```ts import { createPermissionBasedMcpServer } from "toolception"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; // Define role-to-toolset mappings const rolePermissions = { @@ -1587,8 +1575,7 @@ const { start, close } = await createPermissionBasedMcpServer({ new McpServer({ name: "rbac-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), }); await start(); diff --git a/examples/custom-endpoints-demo.ts b/examples/custom-endpoints-demo.ts index d582db1..f584031 100644 --- a/examples/custom-endpoints-demo.ts +++ b/examples/custom-endpoints-demo.ts @@ -31,7 +31,7 @@ * curl -X POST http://localhost:3000/api/users -H 'Content-Type: application/json' -d '{"name":"Alice","email":"alice@example.com"}' */ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import { z } from "zod"; import { createMcpServer, diff --git a/package-lock.json b/package-lock.json index b2336d4..1bd6e1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,13 @@ "license": "Apache-2.0", "dependencies": { "@fastify/cors": "^10.0.1", - "@modelcontextprotocol/sdk": "^1.32.1", + "@modelcontextprotocol/node": "^2.1.2", + "@modelcontextprotocol/server": "^2.4.0", "fastify": "^5.12.5", - "zod": "^3.25.76" + "zod": "^4.6.7" }, "devDependencies": { + "@modelcontextprotocol/client": "^2.4.0", "@types/connect": "^3.4.38", "@types/node": "^25.0.0", "@vitest/coverage-v8": "^5.0.3", @@ -662,9 +664,9 @@ } }, "node_modules/@hono/node-server": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.4.tgz", - "integrity": "sha512-2g1qeHl4BduwDn6L8nkAvDrPUWPMlkQ//a5NhP/zIyaXKMjZNRV+cG9Csu+BiJFf6+68pqzjf4Hi5sJGKHuS1g==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.6.tgz", + "integrity": "sha512-p/KH9MnfNcsm3sR6x4lNgm5j+QN4Et8srKredl9DZfXiyZ1UQ3aQ2YVpFLbqktYN5JI+DB+VZFKM2PqE5hLKjQ==", "license": "MIT", "engines": { "node": ">=20" @@ -702,25 +704,25 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.59.4", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.59.4.tgz", - "integrity": "sha512-XqseDKIw736qlhQwS9SOpbY4veOiP2Nc7YiWmgXXBmeA3kY9qGXw+Tu7DFpU9FvXywE1z8SvLJKUVxFpcX0kig==", + "version": "7.60.1", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.60.1.tgz", + "integrity": "sha512-1FJ/Z30xLL3pWn6oYSJCjGsnlEWkWivwmVeCB7YrRPe+47J7H54NVR+m/1PIL1RYsh/mZsxm/FwirreD72G76g==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.33.15", + "@microsoft/api-extractor-model": "7.33.16", "@microsoft/tsdoc": "~0.17.0", "@microsoft/tsdoc-config": "~0.18.2", - "@rushstack/node-core-library": "5.25.1", + "@rushstack/node-core-library": "5.25.2", "@rushstack/rig-package": "0.7.4", - "@rushstack/terminal": "0.24.7", - "@rushstack/ts-command-line": "5.3.17", + "@rushstack/terminal": "0.24.8", + "@rushstack/ts-command-line": "5.3.18", "diff": "~8.0.2", "minimatch": "10.2.3", "resolve": "~1.22.1", "semver": "~7.7.4", "source-map": "~0.6.1", - "typescript": "5.9.3" + "typescript": "6.0.3" }, "bin": { "api-extractor": "bin/api-extractor" @@ -730,15 +732,15 @@ } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.33.15", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.15.tgz", - "integrity": "sha512-UpCnItIJwu03iNTWtJVKv8Qbf0MXN7WLQZUail9cpAQmnZiIX36YPRRoRxz0pocwytzUpmJhstsLkyW5tsaHBA==", + "version": "7.33.16", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.16.tgz", + "integrity": "sha512-MO61eHRI3DDieE9pxNo5xc096NGUQAIKzEPW6IhNNGU9K4EIhRSYiXfroDZX4joNkrWBMzAMp3WhbPGbHG5zHQ==", "dev": true, "license": "MIT", "dependencies": { "@microsoft/tsdoc": "~0.17.0", "@microsoft/tsdoc-config": "~0.18.2", - "@rushstack/node-core-library": "5.25.1" + "@rushstack/node-core-library": "5.25.2" }, "engines": { "node": ">=20.9.0" @@ -783,6 +785,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@microsoft/tsdoc": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.17.1.tgz", @@ -803,44 +819,64 @@ "resolve": "~1.22.2" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.32.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz", - "integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==", - "license": "MIT", + "node_modules/@modelcontextprotocol/client": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.4.0.tgz", + "integrity": "sha512-LgrD2OphHuG12vfKFKz6SOnq7bKHQmhGJIAYdD+MZBn2npHEyry6uD8zvktodtl5CQM85MO0anUyD9JwmUmxww==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@hono/node-server": "^1.19.9 || ^2.0.5", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", + "@modelcontextprotocol/core": "2.4.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", + "eventsource-parser": "^3.0.8", "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" + "zod": "^4.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.4.0.tgz", + "integrity": "sha512-z8QBKqdfXLPzkLQuG4xp8hrVEMm1PZ1IgxMN2sMbJiFiAKOWXnAzogXn2W4aqIBlP1ea6U5yiqVquvUNM1Cj6Q==", + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/node": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.1.2.tgz", + "integrity": "sha512-IIcIqBpr6NCQQs5DgJTgvbSOAw+3iaR/uquqg+e0nXCdudfjPg/80x0TseUnQx14AUAtE4fsJCEpJKPyb6WPVA==", + "license": "Apache-2.0", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.1.3", + "hono": "^4.11.4" + }, + "engines": { + "node": ">=20" }, "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" + "@modelcontextprotocol/server": "^2.4.0" + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.4.0.tgz", + "integrity": "sha512-rgR9yxSaAJ+bcsi/XroT/S2AfPiIWloXAnrdfYSCms9ivVju0ishu/wW0J21Ho6+ZOR1r7RyeRbjiRuF87ix5Q==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.4.0", + "zod": "^4.2.0" }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } + "engines": { + "node": ">=20" } }, "node_modules/@pinojs/redact": { @@ -1230,9 +1266,9 @@ ] }, "node_modules/@rushstack/node-core-library": { - "version": "5.25.1", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.25.1.tgz", - "integrity": "sha512-ozoRDwIx3jStOYpx86rFyXH4SaUR+DVT6HubQ59avLplJkWXSm5InztYH7RrOzzq9PJXTdq9/+lKX06SfkLVsA==", + "version": "5.25.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.25.2.tgz", + "integrity": "sha512-76QGqWRVMtd/KZqhfSOMBgDvxIUsYPhUBhLSorGdn/wwkuESoJ2/cUI3KmfMKrZrE72o8Lau4P8kCBH/omI17Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1304,13 +1340,13 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.24.7", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.24.7.tgz", - "integrity": "sha512-o8J9nHOIwfYb0s0eG2WYhx7ed3YIvBXQHVFxYdXCmVdBOkTkdArJcgVzRS8ewUepmlic4nGgJPjKIVaNKSqEnA==", + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.24.8.tgz", + "integrity": "sha512-VJAq8ZVgnEtXOC+FnBL3YdP5YNBibT9JvRvEuXsqi0ENZ+Ha0uR9CaK2c+4y8hdk/n6Y2SJ6+AZYDfSQmqupWg==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.25.1", + "@rushstack/node-core-library": "5.25.2", "@rushstack/problem-matcher": "0.2.1", "supports-color": "~8.1.1" }, @@ -1326,30 +1362,14 @@ } } }, - "node_modules/@rushstack/terminal/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/@rushstack/ts-command-line": { - "version": "5.3.17", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.17.tgz", - "integrity": "sha512-pIwhWawZXAT0nTXwvcE/InQ7uaVAgSeIzILMb2mBrI01Hz1yEvxrhV2+DE2Fzux8aZY9OJTdLsI+sk9kZDuAJw==", + "version": "5.3.18", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.18.tgz", + "integrity": "sha512-acP68tGx/DJEIQgTqj/xStBRaghN7axV+uAjNasDjVXgHgHaLQ1f16/bbXtw6mNLjURhTORD4fdfbOZM7kyWcw==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.24.7", + "@rushstack/terminal": "0.24.8", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -1620,19 +1640,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1759,43 +1766,6 @@ "dev": true, "license": "MIT" }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/brace-expansion": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", @@ -1806,44 +1776,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/chai": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", @@ -1868,63 +1800,11 @@ "dev": true, "license": "MIT" }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1946,6 +1826,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1959,15 +1840,6 @@ } } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1987,35 +1859,6 @@ "node": ">=0.3.1" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/entities": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", @@ -2029,19 +1872,11 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2054,18 +1889,6 @@ "dev": true, "license": "MIT" }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -2108,12 +1931,6 @@ "@esbuild/win32-x64": "0.27.2" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2124,19 +1941,11 @@ "@types/estree": "^1.0.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -2146,9 +1955,10 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -2164,68 +1974,6 @@ "node": ">=12.0.0" } }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "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.0", - "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.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.1.tgz", - "integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -2279,9 +2027,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "funding": [ { "type": "github", @@ -2295,9 +2043,9 @@ "license": "BSD-3-Clause" }, "node_modules/fastify": { - "version": "5.12.5", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.5.tgz", - "integrity": "sha512-OB2k1dlxs5/NAABqeKV2FUHkSD2BbENsCak8yULVcymn3fHIPDVa9TI3SDnJSWYSllZmSYuZXy2gTnsT+Sut1A==", + "version": "5.12.8", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.8.tgz", + "integrity": "sha512-moP8exb2sCHLUbJkwp+I6C0padhy4D8lp0SdmzY7HhSdZuqm7O/u3o2aq7u8okaQ//33QDzj3G/u+FsSdB8Qzw==", "funding": [ { "type": "github", @@ -2317,7 +2065,7 @@ "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^7.0.0", - "find-my-way": "^9.6.0", + "find-my-way": "^9.10.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.1.0", @@ -2368,9 +2116,9 @@ } }, "node_modules/fastify/node_modules/fast-uri": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.2.1.tgz", - "integrity": "sha512-TmHQgewjHtMq1E5QKA0tOE0yeYGQs25KZC/ziJpubRtWI15W92e6vPFydWOeZBVROSHBYw/QhD9d5OefdD6LDg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.2.3.tgz", + "integrity": "sha512-I1PXwP5eAhlNlcdpjt9CgB59op2ZvZfxw+BkqGFVIUOgNpCVTcTqp3HhTX7brxj5vsip+nSY4nTvff+eIA76hg==", "funding": [ { "type": "github", @@ -2410,31 +2158,10 @@ } } }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/find-my-way": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.9.0.tgz", - "integrity": "sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.10.0.tgz", + "integrity": "sha512-eru+uCPQ2g+nLBkf84MHP/KyNNMar0BDy0j+i8Fb72GF9/YHBZ58q7FODUtfVV7XU9nfRUHXSmaM8NmZcIbZ4Q==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -2445,24 +2172,6 @@ "node": ">=20" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fs-extra": { "version": "11.3.6", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", @@ -2497,48 +2206,12 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-tsconfig": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", @@ -2552,18 +2225,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2581,22 +2242,11 @@ "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2616,50 +2266,14 @@ } }, "node_modules/hono": { - "version": "4.13.13", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz", - "integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==", + "version": "4.13.16", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.16.tgz", + "integrity": "sha512-AOLOEhvrV+iH92xXCnFr/5+3KjPKcI3xP63XJtX5spAnkznPdHijlIsUTpi7/9EhWqrTCekw5c5zh1K+YsVw7A==", "license": "MIT", "engines": { "node": ">=16.9.0" } }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/import-lazy": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", @@ -2670,21 +2284,6 @@ "node": ">=8" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", - "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", @@ -2710,16 +2309,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/jju": { @@ -2730,9 +2324,10 @@ "license": "MIT" }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.15", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.15.tgz", + "integrity": "sha512-qdF6ewcZARtef63fJ9AKKZPgxw+t7USH6HLXYcD4CrdmSvDp79rpJSPFEKc90QBeGS63u1KQtN2MZQHC0yDIzA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -2770,12 +2365,6 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, "node_modules/jsonfile": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", @@ -2886,61 +2475,6 @@ "source-map-js": "^1.2.1" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -3002,6 +2536,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/muggle-string": { @@ -3030,36 +2565,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/obliterator": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", @@ -3089,36 +2594,6 @@ "node": ">=14.0.0" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -3130,6 +2605,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3142,16 +2618,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3220,6 +2686,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.20.0" @@ -3282,48 +2749,6 @@ ], "license": "MIT" }, - "node_modules/proxy-addr": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", - "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", - "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", @@ -3347,30 +2772,6 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -3422,9 +2823,9 @@ } }, "node_modules/ret": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", - "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.1.tgz", + "integrity": "sha512-Z/nyvABwcRnRD2mMP7Kz6G1dFZZqX1mNErOnxF42OCQKE07b9k9hK9SQxHL3LFEZUdzQaLM4dId1E/E6Vm4WhA==", "license": "MIT", "engines": { "node": ">=10" @@ -3491,26 +2892,10 @@ "fsevents": "~2.3.2" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/safe-regex2": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", - "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.2.tgz", + "integrity": "sha512-tAtCGFPPP02VroDVzp4daGi6bC7jWG+41D47GKf18FeH/ubvKQHT9VmhAkvtjNOmJwqtApzWeUpEhkyEYULvmw==", "funding": [ { "type": "github", @@ -3538,12 +2923,6 @@ "node": ">=10" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -3572,67 +2951,17 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -3645,83 +2974,12 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/sonic-boom": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", @@ -3767,15 +3025,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/std-env": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", @@ -3793,6 +3042,22 @@ "node": ">=0.6.19" } }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -3871,15 +3136,6 @@ "node": ">=12" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -3900,37 +3156,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3969,24 +3194,6 @@ "node": ">= 10.0.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vite": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", @@ -4193,6 +3400,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -4217,29 +3425,14 @@ "node": ">=20.11" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25 || ^4" - } } } } diff --git a/package.json b/package.json index 94196e1..7f8a88e 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,13 @@ }, "dependencies": { "@fastify/cors": "^10.0.1", - "@modelcontextprotocol/sdk": "^1.32.1", + "@modelcontextprotocol/node": "^2.1.2", + "@modelcontextprotocol/server": "^2.4.0", "fastify": "^5.12.5", - "zod": "^3.25.76" + "zod": "^4.6.7" }, "devDependencies": { + "@modelcontextprotocol/client": "^2.4.0", "@types/connect": "^3.4.38", "@types/node": "^25.0.0", "@vitest/coverage-v8": "^5.0.3", diff --git a/src/core/AGENTS.md b/src/core/AGENTS.md index 2a2fceb..5eb3de9 100644 --- a/src/core/AGENTS.md +++ b/src/core/AGENTS.md @@ -18,7 +18,7 @@ Central orchestration layer. Manages toolset lifecycle, tool registration, and s 4. **Namespacing applied BEFORE collision check** — `mapAndValidate()` generates safe names first, then checks for conflicts. 5. **Toolset added to activeToolsets AFTER registration** — All tools must register successfully before toolset is marked active. A partial failure removes the tools already registered, so nothing is left callable and the toolset can be enabled again. 6. **`enableToolsets()` batches notifications** — Calls `enableToolset(name, skipNotification=true)` per toolset, sends one `tools/list_changed` notification at the end. The SDK also sends its own `tools/list_changed` for every `registerTool()` and `remove()` call on a connected server, so a disable of N tools sends N+1. -7. **JSON Schema `inputSchema` uses `registerTool` with a passthrough object** — The SDK rejects plain JSON Schema as a tool schema. Zod raw shapes and schemas are passed to `registerTool` as is. Clients see an open object schema for JSON Schema tools, not the declared one. +7. **`inputSchema` goes through `toRegistrableInputSchema()`** — A Zod 4 schema instance is used as is; a raw shape of Zod 4 values is wrapped with toolception's own `z.object()` (so the SDK never wraps it with a second copy of Zod); a Zod 3 schema throws `ToolingError` `E_VALIDATION` (SDK v2 cannot read it) and the enable fails; anything else is treated as plain JSON Schema and registers as `z.looseObject({})`, so clients see an open object schema, not the declared one, and arguments are not validated. 8. **Failed tool registration at startup makes the orchestrator not ready** — `enableToolsets()` reports failures in its result and never throws. A failure that carries a `code` (set when enabling threw, normally while registering the toolset's tools, e.g. a tool name collision) becomes `initError`, so `isReady()` is `false` and `ensureReady()` throws. Refusals made before any registration (invalid name, already enabled, exposure policy) carry no code and are ignored. Module loader errors are also ignored: `ModuleResolver` logs them and continues. ## Enable Toolset Flow @@ -34,7 +34,7 @@ enableToolset(name) ## SDK registration -Tools are registered with `McpServer.registerTool` (the 5-arg `server.tool()` overloads are deprecated). Before 1.28 the SDK silently treated a JSON Schema `inputSchema` as annotations, so handlers received the request `extra` instead of arguments; newer SDKs throw. `isZodInputSchema()` routes Zod shapes straight through and everything else to the passthrough object. +Tools are registered with `McpServer.registerTool` (`server.tool()` no longer exists in SDK v2). The SDK does not accept a plain JSON Schema object as `inputSchema`, so `toRegistrableInputSchema()` maps every tool schema to a Zod 4 schema first. Calling a tool that is not (or no longer) registered makes the SDK answer with a JSON-RPC error, not an `isError` result. ## Anti-patterns diff --git a/src/core/DynamicToolManager.ts b/src/core/DynamicToolManager.ts index 7f8cf79..665cfa8 100644 --- a/src/core/DynamicToolManager.ts +++ b/src/core/DynamicToolManager.ts @@ -1,9 +1,5 @@ -import type { - McpServer, - RegisteredTool, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer, RegisteredTool } from "@modelcontextprotocol/server"; import { z } from "zod"; -import type { ZodRawShapeCompat } from "@modelcontextprotocol/sdk/server/zod-compat.js"; import type { ExposurePolicy, McpToolDefinition, @@ -251,12 +247,7 @@ export class DynamicToolManager { const hasAnnotations = tool.annotations && Object.keys(tool.annotations).length > 0; - // SDK >= 1.28 rejects plain JSON Schema inputSchema; older versions silently - // treated it as annotations and dropped the args. Non-Zod schemas register - // with a passthrough object so arguments still reach the handler. - const inputSchema = isZodInputSchema(tool.inputSchema) - ? (tool.inputSchema as ZodRawShapeCompat) - : z.object({}).passthrough(); + const inputSchema = toRegistrableInputSchema(tool.name, tool.inputSchema); const handle = this.server.registerTool( tool.name, @@ -397,16 +388,65 @@ export class DynamicToolManager { } } +/** The schema type handed to `registerTool`; arguments arrive as a plain record. */ +type InputSchema = z.ZodType>; + +/** + * @param value - Any value + * @returns True when the value is a Zod 4 schema instance + */ +function isZod4Type(value: unknown): value is z.ZodType { + return typeof value === "object" && value !== null && "_zod" in value; +} + +/** + * @param value - Any value + * @returns True when the value is a Zod 3 schema instance (has `_def` but not `_zod`) + */ +function isZod3Type(value: unknown): boolean { + return ( + typeof value === "object" && + value !== null && + "_def" in value && + !("_zod" in value) + ); +} + /** - * True when the schema is a Zod schema instance (v3 or v4) or a raw shape whose - * values are all Zod schemas. An empty object counts as an empty raw shape. + * Turns a tool's `inputSchema` into the Zod 4 object schema that + * `McpServer.registerTool` expects. + * + * - A Zod 4 schema instance is used as is. + * - A raw shape of Zod 4 schemas (an empty object counts as an empty shape) is + * wrapped with toolception's own `z.object()`, so the SDK never wraps it with + * a second copy of Zod. + * - Anything else is treated as plain JSON Schema and registers as an open + * object, so arguments still reach the handler. * + * @param toolName - The tool name, used in the error message * @param schema - The tool's inputSchema + * @returns A schema to pass to `registerTool` + * @throws ToolingError `E_VALIDATION` when the schema or a shape value is a Zod 3 schema */ -function isZodInputSchema(schema: unknown): boolean { - const isZodType = (v: unknown) => - typeof v === "object" && v !== null && ("_def" in v || "_zod" in v); - if (typeof schema !== "object" || schema === null) return false; - if (isZodType(schema)) return true; - return Object.values(schema).every(isZodType); +function toRegistrableInputSchema( + toolName: string, + schema: unknown +): InputSchema { + if (typeof schema !== "object" || schema === null) return z.looseObject({}); + const values = isZod4Type(schema) || isZod3Type(schema) + ? [schema] + : Object.values(schema); + if (values.some(isZod3Type)) { + throw new ToolingError( + `Tool '${toolName}' uses a Zod 3 schema. toolception requires Zod 4 (zod ^4.6.7): MCP SDK v2 cannot read Zod 3 schemas.`, + "E_VALIDATION" + ); + } + // Casts: tool arguments reach the handler as `Record`; the + // SDK checks object-ness itself when it converts the schema for `tools/list`. + if (isZod4Type(schema)) return schema as InputSchema; + if (values.every(isZod4Type)) { + return z.object(schema as Record) as InputSchema; + } + return z.looseObject({}); } diff --git a/src/core/ServerOrchestrator.ts b/src/core/ServerOrchestrator.ts index d9aee63..d7b9433 100644 --- a/src/core/ServerOrchestrator.ts +++ b/src/core/ServerOrchestrator.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer } from "@modelcontextprotocol/server"; import { ToolsetValidator } from "../mode/ToolsetValidator.js"; import { ModuleResolver } from "../mode/ModuleResolver.js"; import { DynamicToolManager } from "./DynamicToolManager.js"; diff --git a/src/core/core.types.ts b/src/core/core.types.ts index dae6528..4cea64b 100644 --- a/src/core/core.types.ts +++ b/src/core/core.types.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer } from "@modelcontextprotocol/server"; import type { ExposurePolicy, ModuleLoader, Mode, ToolSetCatalog } from "../types/index.js"; import type { ModuleResolver } from "../mode/ModuleResolver.js"; import type { ToolRegistry } from "./ToolRegistry.js"; diff --git a/src/http/AGENTS.md b/src/http/AGENTS.md index b84f7f5..01fe3b5 100644 --- a/src/http/AGENTS.md +++ b/src/http/AGENTS.md @@ -24,7 +24,7 @@ Provides Fastify-based HTTP transport for the MCP protocol. Handles SSE streams, ## Invariants 1. **`mcp-client-id` header required** — All `/mcp` endpoints reject without it (400). Custom endpoints auto-generate `anon-${UUID}` fallback instead. -2. **Session created on POST /mcp initialize** — `isInitializeRequest()` from MCP SDK detects first-contact requests. New session ID generated via `randomUUID()`, stored in `bundle.sessions` Map. +2. **Session created on POST /mcp initialize** — `isInitializeRequest()` from `@modelcontextprotocol/server` detects first-contact requests. New session ID generated via `randomUUID()`, stored in `bundle.sessions` Map. 3. **Cache key uses `resolveSessionContext()`** — All three `/mcp` handlers (POST, GET, DELETE) must derive the cache key through the same `resolveSessionContext()` path. Using plain `clientId` causes cache misses when session context is active. 4. **Origin check is opt-in and `/mcp`-only** — `allowedOrigins` unset means no check (backward compatible). When set: no `Origin` header → allowed (non-browser clients), listed → allowed, anything else (including `null`, which can never be listed) → 403 before the `mcp-client-id` check. Invalid entries throw at transport construction. Both transports must keep the same behaviour, and `cors` narrows to the same list. 5. **Reserved paths cannot be overridden** — `/mcp`, `/healthz`, `/tools`, `/.well-known/mcp-config` are registered before custom endpoints. @@ -36,10 +36,10 @@ Provides Fastify-based HTTP transport for the MCP protocol. Handles SSE streams, → isInitializeRequest(body) detects first contact → drainExistingSessions() — closes sessions in this bundle's map (same-client reconnect) → disconnectServer() — closes the bundle server's current transport (stale same-client connection) - → Set transport.onclose BEFORE server.connect() (SDK 1.26+ chain requirement) + → Set transport.onclose BEFORE server.connect() (SDK chain requirement) → await bundle.orchestrator.isReady() — STATIC bundles preload tools async; SDK refuses capability registration after connect → not ready → drop the bundle from the cache, respond 500 / JSON-RPC -32603; never connect with partial tools (no negative caching: each retry re-runs the module loaders) - → server.connect(transport) — throws "Already connected" if above steps missed + → server.connect(transport) — throws `SdkError` `AlreadyConnected` if above steps missed → Generate session ID, store in bundle.sessions via onsessioninitialized 2. GET /mcp (streaming) @@ -55,9 +55,9 @@ Provides Fastify-based HTTP transport for the MCP protocol. Handles SSE streams, → Remove session from bundle ``` -## SDK 1.26+ Reconnection Protocol +## SDK Reconnection Protocol -`Protocol.connect()` throws `"Already connected"` if `this._transport` is still set. Two private methods handle the pre-connect cleanup: +`Protocol.connect()` throws `AlreadyConnected` ("call close() first") if `this._transport` is still set. Two private methods handle the pre-connect cleanup: - **`drainExistingSessions(sessions)`** — covers same-bundle reconnects where sessions are still in the map (e.g. client aborted without DELETE but same `clientId`) - **`disconnectServer(server)`** — clears a bundle server still attached to a transport that left without DELETE; `StreamableHTTPClientTransport.close()` does NOT send DELETE (it only aborts connections), so `_transport` can remain set. Bundles never share a server, so this only ever closes the same client's stale connection diff --git a/src/http/FastifyTransport.ts b/src/http/FastifyTransport.ts index 23648b4..1f24059 100644 --- a/src/http/FastifyTransport.ts +++ b/src/http/FastifyTransport.ts @@ -11,9 +11,9 @@ import type { ServerOrchestrator } from "../core/ServerOrchestrator.js"; import { ClientResourceCache } from "../session/ClientResourceCache.js"; import type { SessionContextResolver } from "../session/SessionContextResolver.js"; import type { SessionRequestContext } from "../types/index.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/server"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import type { FastifyTransportOptions, CreateBundleCallback, @@ -55,7 +55,7 @@ export class FastifyTransport { private readonly clientCache = new ClientResourceCache<{ server: McpServer; orchestrator: ServerOrchestrator; - sessions: Map; + sessions: Map; }>({ onEvict: (_key, bundle) => { // Clean up all sessions when a client bundle is evicted @@ -233,21 +233,21 @@ export class FastifyTransport { const sessionId = req.headers["mcp-session-id"] as string | undefined; - let transport: StreamableHTTPServerTransport | undefined; + let transport: NodeStreamableHTTPServerTransport | undefined; if (sessionId && bundle.sessions.get(sessionId)) { transport = bundle.sessions.get(sessionId)!; } else if (!sessionId && isInitializeRequest(req.body)) { await this.drainExistingSessions(bundle.sessions); await this.disconnectServer(bundle.server); const newSessionId = randomUUID(); - transport = new StreamableHTTPServerTransport({ + transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => newSessionId, onsessioninitialized: (sid: string) => { bundle!.sessions.set(sid, transport!); }, }); // Set onclose BEFORE connect() so Protocol.connect() captures it in its - // chain. SDK 1.26+ wraps the existing onclose alongside _onclose(), so + // chain. The SDK wraps the existing onclose alongside _onclose(), so // _transport is cleared on disconnect only if this ordering is preserved. transport.onclose = () => { if (transport?.sessionId) @@ -399,7 +399,7 @@ export class FastifyTransport { /** * Closes all active sessions and clears the session map so the server is - * free to accept a new connection. Required for SDK 1.26+, which throws + * free to accept a new connection. Required because the SDK throws * "Already connected" when `connect()` is called while a transport is * still attached. Handles unclean client disconnects followed by re-init. * @@ -409,7 +409,7 @@ export class FastifyTransport { * @param sessions - The client's active session map to drain */ private async drainExistingSessions( - sessions: Map + sessions: Map ): Promise { if (sessions.size === 0) return; const existing = Array.from(sessions.values()); @@ -441,7 +441,7 @@ export class FastifyTransport { private cleanupBundle(bundle: { server: McpServer; orchestrator: ServerOrchestrator; - sessions: Map; + sessions: Map; }): void { for (const [sessionId, transport] of bundle.sessions.entries()) { transport.close().catch((err: unknown) => { diff --git a/src/http/http.types.ts b/src/http/http.types.ts index 5cffd37..7de17c2 100644 --- a/src/http/http.types.ts +++ b/src/http/http.types.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { FastifyInstance, FastifyRequest } from "fastify"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer } from "@modelcontextprotocol/server"; import type { ServerOrchestrator } from "../core/ServerOrchestrator.js"; import type { SessionContextResolver } from "../session/SessionContextResolver.js"; diff --git a/src/http/http.utils.ts b/src/http/http.utils.ts index b02edc5..fe72ead 100644 --- a/src/http/http.utils.ts +++ b/src/http/http.utils.ts @@ -297,7 +297,7 @@ export function createValidationError( error: { code: "VALIDATION_ERROR", message: `Validation failed for ${field}`, - details: error.errors, + details: error.issues, }, }; } diff --git a/src/meta/registerMetaTools.ts b/src/meta/registerMetaTools.ts index 88e6dbf..acb667b 100644 --- a/src/meta/registerMetaTools.ts +++ b/src/meta/registerMetaTools.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer } from "@modelcontextprotocol/server"; import type { Mode } from "../types/index.js"; import { z } from "zod"; import { DynamicToolManager } from "../core/DynamicToolManager.js"; @@ -45,7 +45,7 @@ export function registerMetaTools( "enable_toolset", { description: "Enable a toolset by name", - inputSchema: { name: z.string().describe("Toolset name") }, + inputSchema: z.object({ name: z.string().describe("Toolset name") }), annotations: { destructiveHint: false, idempotentHint: true }, }, async (args: { name: string }) => { @@ -61,7 +61,7 @@ export function registerMetaTools( "disable_toolset", { description: "Disable a toolset by name and remove its tools", - inputSchema: { name: z.string().describe("Toolset name") }, + inputSchema: z.object({ name: z.string().describe("Toolset name") }), annotations: { destructiveHint: false, idempotentHint: true }, }, async (args: { name: string }) => { @@ -77,7 +77,7 @@ export function registerMetaTools( "list_toolsets", { description: "List available toolsets with active status and definitions", - inputSchema: {}, + inputSchema: z.object({}), annotations: { readOnlyHint: true, idempotentHint: true }, }, async () => { @@ -112,7 +112,7 @@ export function registerMetaTools( "describe_toolset", { description: "Describe a toolset with definition, active status and tools", - inputSchema: { name: z.string().describe("Toolset name") }, + inputSchema: z.object({ name: z.string().describe("Toolset name") }), annotations: { readOnlyHint: true, idempotentHint: true }, }, async (args: { name: string }) => { @@ -152,7 +152,7 @@ export function registerMetaTools( "list_tools", { description: "List currently registered tool names (best effort)", - inputSchema: {}, + inputSchema: z.object({}), annotations: { readOnlyHint: true, idempotentHint: true }, }, async () => { diff --git a/src/permissions/PermissionAwareFastifyTransport.ts b/src/permissions/PermissionAwareFastifyTransport.ts index c93cf0e..e8fe86b 100644 --- a/src/permissions/PermissionAwareFastifyTransport.ts +++ b/src/permissions/PermissionAwareFastifyTransport.ts @@ -8,9 +8,9 @@ import { randomUUID } from "node:crypto"; import { z } from "zod"; import type { DynamicToolManager } from "../core/DynamicToolManager.js"; import { ClientResourceCache } from "../session/ClientResourceCache.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/server"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import type { ServerOrchestrator } from "../core/ServerOrchestrator.js"; import type { ClientRequestContext, @@ -54,7 +54,7 @@ export class PermissionAwareFastifyTransport { private readonly clientCache = new ClientResourceCache<{ server: McpServer; orchestrator: ServerOrchestrator; - sessions: Map; + sessions: Map; allowedToolsets: string[]; failedToolsets: string[]; }>({ @@ -180,7 +180,7 @@ export class PermissionAwareFastifyTransport { /** * Closes all active sessions and clears the session map so the server is - * free to accept a new connection. Required for SDK 1.26+, which throws + * free to accept a new connection. Required because the SDK throws * "Already connected" when `connect()` is called while a transport is * still attached. Handles unclean client disconnects followed by re-init. * @@ -190,7 +190,7 @@ export class PermissionAwareFastifyTransport { * @param sessions - The client's active session map to drain */ async #drainExistingSessions( - sessions: Map + sessions: Map ): Promise { if (sessions.size === 0) return; const existing = Array.from(sessions.values()); @@ -221,7 +221,7 @@ export class PermissionAwareFastifyTransport { #cleanupBundle(bundle: { server: McpServer; orchestrator: ServerOrchestrator; - sessions: Map; + sessions: Map; allowedToolsets: string[]; failedToolsets: string[]; }): void { @@ -318,7 +318,7 @@ export class PermissionAwareFastifyTransport { ); } - const providedSessions = (created as { sessions?: Map }).sessions; + const providedSessions = (created as { sessions?: Map }).sessions; bundle = { server: created.server, orchestrator: created.orchestrator, @@ -341,21 +341,21 @@ export class PermissionAwareFastifyTransport { const sessionId = req.headers["mcp-session-id"] as string | undefined; - let transport: StreamableHTTPServerTransport | undefined; + let transport: NodeStreamableHTTPServerTransport | undefined; if (sessionId && bundle.sessions.get(sessionId)) { transport = bundle.sessions.get(sessionId)!; } else if (!sessionId && isInitializeRequest(req.body)) { await this.#drainExistingSessions(bundle.sessions); await this.#disconnectServer(bundle.server); const newSessionId = randomUUID(); - transport = new StreamableHTTPServerTransport({ + transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => newSessionId, onsessioninitialized: (sid: string) => { bundle!.sessions.set(sid, transport!); }, }); // Set onclose BEFORE connect() so Protocol.connect() captures it in its - // chain. SDK 1.26+ wraps the existing onclose alongside _onclose(), so + // chain. The SDK wraps the existing onclose alongside _onclose(), so // _transport is cleared on disconnect only if this ordering is preserved. transport.onclose = () => { if (transport?.sessionId) diff --git a/src/permissions/permissions.types.ts b/src/permissions/permissions.types.ts index 89485c6..abda134 100644 --- a/src/permissions/permissions.types.ts +++ b/src/permissions/permissions.types.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer } from "@modelcontextprotocol/server"; import type { ServerOrchestrator } from "../core/ServerOrchestrator.js"; import type { CustomEndpointDefinition } from "../http/http.types.js"; diff --git a/src/permissions/permissions.utils.ts b/src/permissions/permissions.utils.ts index b2f85ed..88eb393 100644 --- a/src/permissions/permissions.utils.ts +++ b/src/permissions/permissions.utils.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer } from "@modelcontextprotocol/server"; import type { ExposurePolicy, PermissionConfig } from "../types/index.js"; import type { ServerOrchestrator } from "../core/ServerOrchestrator.js"; import type { PermissionResolver } from "./PermissionResolver.js"; diff --git a/src/server/createMcpServer.ts b/src/server/createMcpServer.ts index 21cca7c..8a51293 100644 --- a/src/server/createMcpServer.ts +++ b/src/server/createMcpServer.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer } from "@modelcontextprotocol/server"; import type { Mode } from "../types/index.js"; import type { CreateBundleCallback } from "../http/http.types.js"; import type { CreateMcpServerOptions, McpServerHandle } from "./server.types.js"; diff --git a/src/server/createPermissionBasedMcpServer.ts b/src/server/createPermissionBasedMcpServer.ts index ac3f04e..110ff6b 100644 --- a/src/server/createPermissionBasedMcpServer.ts +++ b/src/server/createPermissionBasedMcpServer.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer } from "@modelcontextprotocol/server"; import type { ExposurePolicy } from "../types/index.js"; import type { CreatePermissionBasedMcpServerOptions, diff --git a/src/server/server.types.ts b/src/server/server.types.ts index 4fec221..5d63cab 100644 --- a/src/server/server.types.ts +++ b/src/server/server.types.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer } from "@modelcontextprotocol/server"; import type { ExposurePolicy, Mode, diff --git a/src/types/AGENTS.md b/src/types/AGENTS.md index 2d182b6..4c0a3c0 100644 --- a/src/types/AGENTS.md +++ b/src/types/AGENTS.md @@ -23,7 +23,7 @@ Also: `src/errors/ToolingError.ts` (18 LOC) — thin error class wrapping Toolin 2. **ToolingErrorCode values are exhaustive** — All error codes defined here; do not add codes elsewhere 3. **PermissionConfig requires source field** — Must specify `"headers"` or `"config"` 4. **SessionContextConfig.allowedKeys is security-critical** — Always whitelist allowed keys; omitting allows arbitrary config injection -5. **`McpToolDefinition.inputSchema` should be Zod** — A Zod raw shape or Zod object schema. `McpServer.registerTool` does not accept JSON Schema, so `DynamicToolManager` registers JSON Schema tools with an open passthrough object (args unvalidated, properties not advertised) +5. **`McpToolDefinition.inputSchema` should be Zod 4** — A Zod 4 raw shape or Zod 4 object schema. A Zod 3 schema fails the toolset's enable with `E_VALIDATION`. `McpServer.registerTool` does not accept JSON Schema, so `DynamicToolManager` registers JSON Schema tools with an open object (`z.looseObject({})`: args unvalidated, properties not advertised) ## Anti-patterns diff --git a/src/types/index.ts b/src/types/index.ts index 34cb2f6..74cdcd2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,9 +4,10 @@ export type McpToolDefinition = { name: string; description: string; /** - * Zod raw shape (e.g. `{ symbol: z.string() }`) or Zod object schema, as accepted - * by `McpServer.registerTool`. A plain JSON Schema object is registered as an - * open passthrough object: clients don't see its properties and args are not validated. + * Zod 4 raw shape (e.g. `{ symbol: z.string() }`) or Zod 4 object schema. A raw + * shape is wrapped with `z.object()` before it reaches `McpServer.registerTool`; + * a Zod 3 schema is refused (`E_VALIDATION`). A plain JSON Schema object is + * registered as an open object: clients don't see its properties and args are not validated. */ inputSchema: Record; handler: (args: any) => Promise | any; diff --git a/tests/createPermissionBasedMcpServer.test.ts b/tests/createPermissionBasedMcpServer.test.ts index 758b3ac..820908f 100644 --- a/tests/createPermissionBasedMcpServer.test.ts +++ b/tests/createPermissionBasedMcpServer.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi } from "vitest"; import { createPermissionBasedMcpServer } from "../src/server/createPermissionBasedMcpServer.js"; import type { CreatePermissionBasedMcpServerOptions } from "../src/types/index.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; describe("createPermissionBasedMcpServer", () => { const createBasicOptions = (): CreatePermissionBasedMcpServerOptions => ({ @@ -9,8 +9,7 @@ describe("createPermissionBasedMcpServer", () => { new McpServer({ name: "test-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), catalog: { toolsetA: { name: "Toolset A", @@ -146,8 +145,7 @@ describe("createPermissionBasedMcpServer", () => { new McpServer({ name: "test-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), catalog: { toolsetA: { name: "Toolset A", @@ -175,8 +173,7 @@ describe("createPermissionBasedMcpServer", () => { new McpServer({ name: "test-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), catalog: { toolsetA: { name: "Toolset A", @@ -217,8 +214,7 @@ describe("createPermissionBasedMcpServer", () => { new McpServer({ name: "test-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), catalog: { toolsetA: { name: "Toolset A", @@ -248,8 +244,7 @@ describe("createPermissionBasedMcpServer", () => { new McpServer({ name: "test-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), catalog: { toolsetA: { name: "Toolset A", @@ -310,7 +305,7 @@ describe("createPermissionBasedMcpServer", () => { const result = await createPermissionBasedMcpServer(options); expect(result.server).toBeInstanceOf(McpServer); - expect(result.server).toHaveProperty("tool"); + expect(result.server).toHaveProperty("registerTool"); }); }); @@ -320,8 +315,7 @@ describe("createPermissionBasedMcpServer", () => { new McpServer({ name: "test-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }) + }, { capabilities: { tools: { listChanged: false } } }) ); const options: CreatePermissionBasedMcpServerOptions = { @@ -353,8 +347,7 @@ describe("createPermissionBasedMcpServer", () => { new McpServer({ name: "test-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), catalog: { toolsetA: { name: "Toolset A", @@ -386,8 +379,7 @@ describe("createPermissionBasedMcpServer", () => { new McpServer({ name: "test-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), catalog: { toolsetA: { name: "Toolset A", @@ -426,8 +418,7 @@ describe("createPermissionBasedMcpServer", () => { new McpServer({ name: "test-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }), + }, { capabilities: { tools: { listChanged: false } } }), catalog: { toolsetA: { name: "Toolset A", diff --git a/tests/customEndpoints.integration.test.ts b/tests/customEndpoints.integration.test.ts index 1889f7e..b3bf36a 100644 --- a/tests/customEndpoints.integration.test.ts +++ b/tests/customEndpoints.integration.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { z } from "zod"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import Fastify, { type FastifyInstance } from "fastify"; import { createMcpServer, defineEndpoint } from "../src/index.js"; import { createPermissionBasedMcpServer, definePermissionAwareEndpoint } from "../src/index.js"; diff --git a/tests/dependencies.test.ts b/tests/dependencies.test.ts new file mode 100644 index 0000000..a54bd50 --- /dev/null +++ b/tests/dependencies.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; + +/** + * toolception wraps tool schemas with its own `zod` and hands them to the SDK, + * so a second copy of `zod` or `@modelcontextprotocol/server` in the tree would + * make `instanceof` and schema detection fail in ways that only show at runtime. + */ +interface Lockfile { + readonly packages: Record; +} + +const lockfile = JSON.parse( + readFileSync(new URL("../package-lock.json", import.meta.url), "utf8") +) as Lockfile; + +/** @returns Every lockfile path of the named package, nested copies included */ +function copiesOf(name: string): string[] { + return Object.keys(lockfile.packages).filter( + (path) => path === `node_modules/${name}` || path.endsWith(`/node_modules/${name}`) + ); +} + +describe("dependency tree", () => { + it.each(["zod", "@modelcontextprotocol/server", "@modelcontextprotocol/core"])( + "has exactly one copy of %s", + (name) => { + expect(copiesOf(name)).toEqual([`node_modules/${name}`]); + } + ); + + it("resolves Zod 4 and SDK v2", () => { + expect(lockfile.packages["node_modules/zod"]?.version).toMatch(/^4\./); + expect( + lockfile.packages["node_modules/@modelcontextprotocol/server"]?.version + ).toMatch(/^2\./); + }); +}); diff --git a/tests/dynamicToolManager.test.ts b/tests/dynamicToolManager.test.ts index 7212d27..8b4989d 100644 --- a/tests/dynamicToolManager.test.ts +++ b/tests/dynamicToolManager.test.ts @@ -1,8 +1,7 @@ import { describe, it, expect, vi } from "vitest"; import { z } from "zod"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { Client } from "@modelcontextprotocol/client"; +import { McpServer, InMemoryTransport } from "@modelcontextprotocol/server"; import { DynamicToolManager } from "../src/core/DynamicToolManager.js"; import { ModuleResolver } from "../src/mode/ModuleResolver.js"; import { ToolRegistry } from "../src/core/ToolRegistry.js"; @@ -341,6 +340,80 @@ describe("DynamicToolManager with a real McpServer", () => { }); }); +describe("DynamicToolManager input schema handling (Zod 4)", () => { + async function enable(inputSchema: unknown) { + const server = new McpServer({ name: "t", version: "1.0.0" }); + const resolver = new ModuleResolver({ + catalog: { + core: { + name: "Core", + description: "", + tools: [ + { + name: "echo", + description: "Echo", + inputSchema, + handler: async (args: { text?: string }) => ({ + content: [{ type: "text", text: args.text ?? "none" }], + }), + }, + ], + }, + } as any, + }); + const manager = new DynamicToolManager({ + server, + resolver, + toolRegistry: new ToolRegistry({ namespaceWithToolset: false }), + }); + const result = await manager.enableToolset("core"); + const [serverSide, clientSide] = InMemoryTransport.createLinkedPair(); + await server.connect(serverSide); + const client = new Client({ name: "c", version: "1.0.0" }); + await client.connect(clientSide); + return { result, client }; + } + + it("uses a Zod 4 object schema instance as is", async () => { + const { result, client } = await enable(z.object({ text: z.string() })); + expect(result.success).toBe(true); + const { tools } = await client.listTools(); + expect(tools[0].inputSchema.properties).toHaveProperty("text"); + const res = await client.callTool({ name: "echo", arguments: { text: "hi" } }); + expect((res.content as any)[0].text).toBe("hi"); + }); + + it("validates arguments against a Zod 4 raw shape", async () => { + const { client } = await enable({ text: z.string() }); + const res = await client.callTool({ name: "echo", arguments: { text: 42 } }); + expect(res.isError).toBe(true); + expect((res.content as any)[0].text).toMatch(/invalid|validation/i); + }); + + it("registers an empty shape as a tool without arguments", async () => { + const { result, client } = await enable({}); + expect(result.success).toBe(true); + const res = await client.callTool({ name: "echo", arguments: {} }); + expect((res.content as any)[0].text).toBe("none"); + }); + + it("refuses a Zod 3 schema with a clear error, and registers nothing", async () => { + // A Zod 3 type has `_def` but no `_zod`. + const zod3Like = { _def: { typeName: "ZodString" }, parse: () => "" }; + const { result, client } = await enable({ text: zod3Like }); + expect(result.success).toBe(false); + expect(result.code).toBe("E_VALIDATION"); + expect(result.message).toMatch(/Tool 'echo' uses a Zod 3 schema.*Zod 4/); + expect((await client.listTools()).tools).toEqual([]); + }); + + it("refuses a Zod 3 schema instance as the whole inputSchema", async () => { + const { result } = await enable({ _def: { typeName: "ZodObject" } }); + expect(result.success).toBe(false); + expect(result.code).toBe("E_VALIDATION"); + }); +}); + describe("DynamicToolManager disable removes tools", () => { const ping = { name: "ping", @@ -356,7 +429,7 @@ describe("DynamicToolManager disable removes tools", () => { { name: "t", version: "1.0.0" }, { capabilities: { tools: { listChanged: true } } } ); - server.registerTool("meta", { inputSchema: {} }, async () => ({ content: [] })); + server.registerTool("meta", { inputSchema: z.object({}) }, async () => ({ content: [] })); return server; } @@ -387,9 +460,10 @@ describe("DynamicToolManager disable removes tools", () => { expect(res.message).toContain("Removed 1 tools"); expect(await listedNames(client)).toEqual(["meta"]); - const call = await client.callTool({ name: "core.ping", arguments: {} }); - expect(call.isError).toBe(true); - expect((call.content as any)[0].text).toMatch(/not found/i); + // SDK v2 answers an unknown tool with a JSON-RPC error, not an `isError` result + await expect( + client.callTool({ name: "core.ping", arguments: {} }) + ).rejects.toThrow(/not found/i); }); it("frees the tool names so the toolset can be enabled again", async () => { diff --git a/tests/e2e/dynamicMode.e2e.test.ts b/tests/e2e/dynamicMode.e2e.test.ts index ae39992..12d6de5 100644 --- a/tests/e2e/dynamicMode.e2e.test.ts +++ b/tests/e2e/dynamicMode.e2e.test.ts @@ -1,8 +1,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { createMcpServer } from "../../src/index.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; import { getAvailablePort, testCatalog, @@ -197,12 +196,10 @@ describe("DYNAMIC mode E2E", () => { // Disabled tools are no longer listed and can no longer be called expect(extractToolNames(await client.listTools())).not.toContain("core.ping"); - const staleCall = await client.callTool({ - name: "core.ping", - arguments: {}, - } as any); - expect(staleCall.isError).toBe(true); - expect(extractTextContent(staleCall)).toMatch(/not found/i); + // SDK v2 answers an unknown tool with a JSON-RPC error, not an `isError` result + await expect( + client.callTool({ name: "core.ping", arguments: {} } as any) + ).rejects.toThrow(/not found/i); await client.close(); }); diff --git a/tests/e2e/jsonSchemaTools.e2e.test.ts b/tests/e2e/jsonSchemaTools.e2e.test.ts index f5c6fd1..486ccc0 100644 --- a/tests/e2e/jsonSchemaTools.e2e.test.ts +++ b/tests/e2e/jsonSchemaTools.e2e.test.ts @@ -1,8 +1,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { createMcpServer } from "../../src/index.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; import { getAvailablePort, extractTextContent } from "./helpers.js"; describe("JSON Schema tool definitions E2E", () => { diff --git a/tests/e2e/multiClient.e2e.test.ts b/tests/e2e/multiClient.e2e.test.ts new file mode 100644 index 0000000..a0a21d2 --- /dev/null +++ b/tests/e2e/multiClient.e2e.test.ts @@ -0,0 +1,349 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { + createMcpServer, + createPermissionBasedMcpServer, +} from "../../src/index.js"; +import { + Client, + StreamableHTTPClientTransport, +} from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; +import { + getAvailablePort, + testCatalog, + extractToolNames, + parseToolResponse, + sessionContextCatalog, + sessionContextModuleLoaders, +} from "./helpers.js"; + +/** + * Acceptance gate for the MCP SDK v2 move: every client keeps its own server, + * transport and enabled/disabled toolset state, so one client's changes never + * show up for another client connected at the same time. + */ + +interface Connected { + readonly client: Client; + readonly transport: StreamableHTTPClientTransport; + /** Number of `tools/list_changed` notifications this client has received. */ + listChangedCount(): number; +} + +async function connect( + port: number, + clientId: string, + extraHeaders: Record = {}, + query = "" +): Promise { + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp${query}`), + { + requestInit: { + headers: { "mcp-client-id": clientId, ...extraHeaders }, + }, + } + ); + const client = new Client({ name: "test", version: "1.0.0" }); + let count = 0; + client.setNotificationHandler("notifications/tools/list_changed", () => { + count += 1; + }); + await client.connect(transport); + return { client, transport, listChangedCount: () => count }; +} + +async function toolNames(c: Connected): Promise { + return extractToolNames(await c.client.listTools()).sort(); +} + +async function metaCall(c: Connected, tool: string, name: string) { + return parseToolResponse<{ success: boolean }>( + await c.client.callTool({ name: tool, arguments: { name } } as any) + ); +} + +/** Lets pending SSE notifications reach the clients. */ +const settle = () => new Promise((resolve) => setTimeout(resolve, 100)); + +describe("multiple concurrent clients keep their own toolset state", () => { + describe("DYNAMIC mode", () => { + let server: Awaited>; + let port: number; + let serversCreated = 0; + + beforeAll(async () => { + port = await getAvailablePort(); + server = await createMcpServer({ + createServer: () => { + serversCreated += 1; + return new McpServer( + { name: "multi-dynamic", version: "1.0.0" }, + { capabilities: { tools: { listChanged: true } } } + ); + }, + catalog: testCatalog, + startup: { mode: "DYNAMIC" }, + http: { port }, + }); + await server.start(); + }); + + afterAll(async () => { + await server.close(); + }); + + it("never leaks enable/disable between clients", async () => { + const a = await connect(port, "multi-a1"); + const b = await connect(port, "multi-b1"); + const metaOnly = await toolNames(b); + expect(metaOnly).not.toContain("core.ping"); + + expect((await metaCall(a, "enable_toolset", "core")).success).toBe(true); + expect((await metaCall(a, "enable_toolset", "admin")).success).toBe(true); + expect((await metaCall(a, "disable_toolset", "admin")).success).toBe(true); + + const aTools = await toolNames(a); + expect(aTools).toContain("core.ping"); + expect(aTools).not.toContain("admin.reset"); + expect(await toolNames(b)).toEqual(metaOnly); + + // Now B changes its state; A must not notice. + expect((await metaCall(b, "enable_toolset", "admin")).success).toBe(true); + expect(await toolNames(b)).toContain("admin.reset"); + expect(await toolNames(b)).not.toContain("core.ping"); + expect(await toolNames(a)).toEqual(aTools); + + await a.client.close(); + await b.client.close(); + }); + + it("removes a disabled tool for that client only", async () => { + const a = await connect(port, "multi-a2"); + const b = await connect(port, "multi-b2"); + await metaCall(a, "enable_toolset", "core"); + await metaCall(b, "enable_toolset", "core"); + + await metaCall(a, "disable_toolset", "core"); + + await expect( + a.client.callTool({ name: "core.ping", arguments: {} }) + ).rejects.toThrow(/not found/i); + const stillWorks = await b.client.callTool({ + name: "core.ping", + arguments: {}, + }); + expect((stillWorks.content as any)[0].text).toBe("pong"); + expect(await toolNames(b)).toContain("core.ping"); + + await a.client.close(); + await b.client.close(); + }); + + it("sends tools/list_changed only to the client whose state changed", async () => { + const a = await connect(port, "multi-a3"); + const b = await connect(port, "multi-b3"); + await settle(); + const bBefore = b.listChangedCount(); + const aBefore = a.listChangedCount(); + + await metaCall(a, "enable_toolset", "core"); + await settle(); + + expect(a.listChangedCount()).toBeGreaterThan(aBefore); + expect(b.listChangedCount()).toBe(bBefore); + + await a.client.close(); + await b.client.close(); + }); + + it("keeps state when the same client id reconnects, and rejects other clients' sessions", async () => { + const first = await connect(port, "multi-a4"); + await metaCall(first, "enable_toolset", "core"); + const sessionId = first.transport.sessionId; + expect(sessionId).toBeTruthy(); + const other = await connect(port, "multi-b4"); + + // A different client id must not use A's session. + const hijack = await fetch(`http://localhost:${port}/mcp`, { + method: "POST", + headers: { + "content-type": "application/json", + accept: "application/json, text/event-stream", + "mcp-client-id": "multi-b4", + "mcp-session-id": sessionId as string, + }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }), + }); + expect(hijack.status).toBe(400); + const hijackDelete = await fetch(`http://localhost:${port}/mcp`, { + method: "DELETE", + headers: { + "mcp-client-id": "multi-b4", + "mcp-session-id": sessionId as string, + }, + }); + expect(hijackDelete.status).toBe(404); + expect(await toolNames(other)).not.toContain("core.ping"); + + // The same client id reconnects (without DELETE) and keeps its toolsets. + await first.client.close(); + const again = await connect(port, "multi-a4"); + expect(await toolNames(again)).toContain("core.ping"); + + await again.client.close(); + await other.client.close(); + }); + + it("creates one McpServer per client id, not per request", async () => { + const before = serversCreated; + const a = await connect(port, "multi-a5"); + const b = await connect(port, "multi-b5"); + await toolNames(a); + await toolNames(b); + await toolNames(a); + expect(serversCreated - before).toBe(2); + await a.client.close(); + await b.client.close(); + }); + }); + + describe("DYNAMIC mode with session context", () => { + let server: Awaited>; + let port: number; + + beforeAll(async () => { + port = await getAvailablePort(); + server = await createMcpServer({ + createServer: () => + new McpServer( + { name: "multi-context", version: "1.0.0" }, + { capabilities: { tools: { listChanged: true } } } + ), + catalog: sessionContextCatalog, + moduleLoaders: sessionContextModuleLoaders, + sessionContext: { + enabled: true, + queryParam: { + name: "config", + encoding: "base64", + allowedKeys: ["API_TOKEN"], + }, + merge: "shallow", + }, + startup: { mode: "DYNAMIC" }, + http: { port }, + }); + await server.start(); + }); + + afterAll(async () => { + await server.close(); + }); + + it("never shows one client's token to another client's tools", async () => { + const query = (token: string) => + `?config=${Buffer.from(JSON.stringify({ API_TOKEN: token })).toString("base64")}`; + const a = await connect(port, "ctx-a", {}, query("token-a")); + const b = await connect(port, "ctx-b", {}, query("token-b")); + await metaCall(a, "enable_toolset", "tenant"); + await metaCall(b, "enable_toolset", "tenant"); + + const read = async (c: Connected) => + parseToolResponse<{ API_TOKEN: string }>( + await c.client.callTool({ name: "tenant.get_context", arguments: {} }) + ).API_TOKEN; + + expect(await read(a)).toBe("token-a"); + expect(await read(b)).toBe("token-b"); + expect(await read(a)).toBe("token-a"); + + await a.client.close(); + await b.client.close(); + }); + }); + + describe("STATIC mode", () => { + let server: Awaited>; + let port: number; + + beforeAll(async () => { + port = await getAvailablePort(); + server = await createMcpServer({ + createServer: () => + new McpServer({ name: "multi-static", version: "1.0.0" }), + catalog: testCatalog, + startup: { mode: "STATIC", toolsets: ["core"] }, + http: { port }, + }); + await server.start(); + }); + + afterAll(async () => { + await server.close(); + }); + + it("serves several clients at once, each from its own bundle", async () => { + const clients = await Promise.all([ + connect(port, "static-multi-1"), + connect(port, "static-multi-2"), + connect(port, "static-multi-3"), + ]); + for (const c of clients) { + expect(await toolNames(c)).toEqual(["core.ping"]); + const res = await c.client.callTool({ name: "core.ping", arguments: {} }); + expect((res.content as any)[0].text).toBe("pong"); + } + // A new client connecting does not drop the earlier ones. + const late = await connect(port, "static-multi-4"); + expect(await toolNames(clients[0])).toEqual(["core.ping"]); + for (const c of [...clients, late]) await c.client.close(); + }); + }); + + describe("permission-based mode", () => { + let server: Awaited>; + let port: number; + + beforeAll(async () => { + port = await getAvailablePort(); + server = await createPermissionBasedMcpServer({ + createServer: () => + new McpServer({ name: "multi-permission", version: "1.0.0" }), + catalog: testCatalog, + permissions: { + source: "config", + staticMap: { "perm-admin": ["core", "admin"], "perm-user": ["core"] }, + defaultPermissions: [], + }, + http: { port }, + }); + await server.start(); + }); + + afterAll(async () => { + await server.close(); + }); + + it("keeps concurrent clients' permitted tools apart", async () => { + const admin = await connect(port, "perm-admin"); + const user = await connect(port, "perm-user"); + + expect(await toolNames(admin)).toEqual(["admin.reset", "core.ping"]); + expect(await toolNames(user)).toEqual(["core.ping"]); + + // The user client cannot reach the admin client's tool. + await expect( + user.client.callTool({ name: "admin.reset", arguments: {} }) + ).rejects.toThrow(/not found/i); + const res = await admin.client.callTool({ + name: "admin.reset", + arguments: {}, + }); + expect((res.content as any)[0].text).toBe("reset done"); + + await admin.client.close(); + await user.client.close(); + }); + }); +}); diff --git a/tests/e2e/permissionBased.e2e.test.ts b/tests/e2e/permissionBased.e2e.test.ts index 133690d..4a42184 100644 --- a/tests/e2e/permissionBased.e2e.test.ts +++ b/tests/e2e/permissionBased.e2e.test.ts @@ -2,9 +2,8 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { createPermissionBasedMcpServer } from "../../src/index.js"; import { PermissionAwareFastifyTransport } from "../../src/permissions/PermissionAwareFastifyTransport.js"; import { ServerOrchestrator } from "../../src/core/ServerOrchestrator.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; import { getAvailablePort, testCatalog, diff --git a/tests/e2e/staticMode.e2e.test.ts b/tests/e2e/staticMode.e2e.test.ts index c3333f7..48e390f 100644 --- a/tests/e2e/staticMode.e2e.test.ts +++ b/tests/e2e/staticMode.e2e.test.ts @@ -1,8 +1,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { createMcpServer } from "../../src/index.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; import { getAvailablePort, testCatalog, diff --git a/tests/originValidation.test.ts b/tests/originValidation.test.ts index 7822848..f0d4d91 100644 --- a/tests/originValidation.test.ts +++ b/tests/originValidation.test.ts @@ -1,8 +1,7 @@ import { describe, it, expect, afterEach } from "vitest"; import Fastify from "fastify"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; import { FastifyTransport } from "../src/http/FastifyTransport.js"; import { PermissionAwareFastifyTransport } from "../src/permissions/PermissionAwareFastifyTransport.js"; import { createMcpServer, createPermissionBasedMcpServer } from "../src/index.js"; diff --git a/tests/permissionAwareFastifyTransport.test.ts b/tests/permissionAwareFastifyTransport.test.ts index 3f63daa..b5600a2 100644 --- a/tests/permissionAwareFastifyTransport.test.ts +++ b/tests/permissionAwareFastifyTransport.test.ts @@ -5,7 +5,7 @@ import type { ClientRequestContext, PermissionAwareBundle, } from "../src/permissions/permissions.types.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import Fastify, { type FastifyInstance } from "fastify"; describe("PermissionAwareFastifyTransport", () => { @@ -24,8 +24,7 @@ describe("PermissionAwareFastifyTransport", () => { mockServer = new McpServer({ name: "test-server", version: "1.0.0", - capabilities: { tools: { listChanged: false } }, - }); + }, { capabilities: { tools: { listChanged: false } } }); mockOrchestrator = { getManager: vi.fn().mockReturnValue({ diff --git a/tests/smoke-e2e/client-demo.ts b/tests/smoke-e2e/client-demo.ts index 211b825..b143105 100644 --- a/tests/smoke-e2e/client-demo.ts +++ b/tests/smoke-e2e/client-demo.ts @@ -1,5 +1,4 @@ -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport, Client } from "@modelcontextprotocol/client"; async function main() { const PORT = Number(process.env.PORT ?? 3003); diff --git a/tests/smoke-e2e/permission-config-client-demo.ts b/tests/smoke-e2e/permission-config-client-demo.ts index 09f2858..1623b8f 100644 --- a/tests/smoke-e2e/permission-config-client-demo.ts +++ b/tests/smoke-e2e/permission-config-client-demo.ts @@ -1,5 +1,4 @@ -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport, Client } from "@modelcontextprotocol/client"; /** * Smoke test client demonstrating config-based permission control. diff --git a/tests/smoke-e2e/permission-config-server-demo.ts b/tests/smoke-e2e/permission-config-server-demo.ts index 9370f64..aad3142 100644 --- a/tests/smoke-e2e/permission-config-server-demo.ts +++ b/tests/smoke-e2e/permission-config-server-demo.ts @@ -1,5 +1,5 @@ import { createPermissionBasedMcpServer } from "../../src/server/createPermissionBasedMcpServer.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { ToolSetCatalog, ModuleLoader } from "../../src/types/index.js"; import { z } from "zod"; @@ -84,8 +84,7 @@ const createServer = () => new McpServer({ name: "toolception-permission-config-demo", version: "0.1.0", - capabilities: { tools: {} }, // STATIC mode, no listChanged - }); + }, { capabilities: { tools: {} } }) // STATIC mode, no listChanged; /** * Static permission map for known clients. diff --git a/tests/smoke-e2e/permission-header-client-demo.ts b/tests/smoke-e2e/permission-header-client-demo.ts index fe44781..e982f9b 100644 --- a/tests/smoke-e2e/permission-header-client-demo.ts +++ b/tests/smoke-e2e/permission-header-client-demo.ts @@ -1,5 +1,4 @@ -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport, Client } from "@modelcontextprotocol/client"; /** * Smoke test client demonstrating header-based permission control. diff --git a/tests/smoke-e2e/permission-header-server-demo.ts b/tests/smoke-e2e/permission-header-server-demo.ts index c9d47d2..3fb52f3 100644 --- a/tests/smoke-e2e/permission-header-server-demo.ts +++ b/tests/smoke-e2e/permission-header-server-demo.ts @@ -1,5 +1,5 @@ import { createPermissionBasedMcpServer } from "../../src/server/createPermissionBasedMcpServer.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { ToolSetCatalog, ModuleLoader } from "../../src/types/index.js"; import { z } from "zod"; @@ -86,8 +86,7 @@ const createServer = () => new McpServer({ name: "toolception-permission-header-demo", version: "0.1.0", - capabilities: { tools: {} }, // STATIC mode, no listChanged - }); + }, { capabilities: { tools: {} } }) // STATIC mode, no listChanged; const { start, close } = await createPermissionBasedMcpServer({ catalog, diff --git a/tests/smoke-e2e/server-demo.ts b/tests/smoke-e2e/server-demo.ts index af18206..88130a8 100644 --- a/tests/smoke-e2e/server-demo.ts +++ b/tests/smoke-e2e/server-demo.ts @@ -1,5 +1,5 @@ import { createMcpServer } from "../../src/server/createMcpServer.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { ToolSetCatalog, ModuleLoader } from "../../src/types/index.js"; import { z } from "zod"; @@ -41,8 +41,7 @@ const createServer = () => new McpServer({ name: "toolception-server-demo", version: "0.1.0", - capabilities: { tools: { listChanged: true } }, - }); + }, { capabilities: { tools: { listChanged: true } } }); const STATIC = (process.env.STARTUP_MODE || "").toUpperCase() === "STATIC"; diff --git a/vite.config.ts b/vite.config.ts index ce14770..41ec6c8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -23,7 +23,7 @@ export default defineConfig({ sourcemap: true, rollupOptions: { external: [ - /^@modelcontextprotocol\/sdk(\/.*)?$/, + /^@modelcontextprotocol\/(server|node)(\/.*)?$/, /^fastify(\/.*)?$/, /^@fastify\/cors(\/.*)?$/, "zod", From ac12cacfa6ec75adce228cc21de29d0aff7abc5b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 12:01:06 +0000 Subject: [PATCH 2/3] fix: address TS review round 1 - demos: statement terminator before the trailing comment - TSDoc: name the AlreadyConnected error instead of the v1 message - DynamicToolManager: summary sentences on the Zod type guards - changelog: VALIDATION_ERROR details carry Zod 4 issues - tests: poll for list_changed instead of a fixed sleep, drop the SDK-internal core package from the single-copy check, and cover rollback of an earlier tool in the Zod 3 refusal test Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01VmUXGSYMuTcM7fquxzRook --- CHANGELOG.md | 1 + src/core/DynamicToolManager.ts | 4 ++++ src/http/FastifyTransport.ts | 2 +- .../PermissionAwareFastifyTransport.ts | 2 +- tests/dependencies.test.ts | 2 +- tests/dynamicToolManager.test.ts | 13 ++++++++++--- tests/e2e/multiClient.e2e.test.ts | 16 +++++++++++----- tests/smoke-e2e/permission-config-server-demo.ts | 2 +- tests/smoke-e2e/permission-header-server-demo.ts | 2 +- 9 files changed, 31 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8dfb25..3aca200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Releases before 0.7.0 are described in [GitHub Releases](https://github.com/code - **Migration:** change your imports to `@modelcontextprotocol/server` (`McpServer`) and `@modelcontextprotocol/client` (`Client`, `StreamableHTTPClientTransport`); install `zod` ^4.6.7; and write tool `inputSchema` values and `defineEndpoint` schemas with Zod 4. `z.record()` needs two arguments in Zod 4. Pass server capabilities as the second `McpServer` argument (`new McpServer(info, { capabilities })`); `capabilities` inside the first argument is rejected by the types. - A tool whose `inputSchema` is a Zod 3 schema (or a raw shape holding one) now fails to enable with an `E_VALIDATION` error that names the tool. - `createServer` factories must return a v2 `McpServer`; the removed `server.tool()`, `server.prompt()` and `server.resource()` are replaced by `registerTool`, `registerPrompt` and `registerResource`. +- `defineEndpoint` / `definePermissionAwareEndpoint` validation failures (`VALIDATION_ERROR`, HTTP 400) now carry Zod 4 issue objects in `error.details`: issue codes and messages differ from Zod 3 (for example `invalid_type` messages). Clients that match on the old text must be updated. - Calling a tool the server no longer lists (for example a disabled toolset's tool) is now answered with a JSON-RPC error ("Tool ... not found") instead of a result with `isError: true`. This is SDK v2 behaviour. - `FastifyTransport` and `PermissionAwareFastifyTransport` use `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`. Session handling, the per-client `mcp-client-id` model and the HTTP endpoints are unchanged: each client still gets its own server, transport and enabled/disabled toolset state. diff --git a/src/core/DynamicToolManager.ts b/src/core/DynamicToolManager.ts index 665cfa8..525025d 100644 --- a/src/core/DynamicToolManager.ts +++ b/src/core/DynamicToolManager.ts @@ -392,6 +392,8 @@ export class DynamicToolManager { type InputSchema = z.ZodType>; /** + * Detects a Zod 4 schema by its `_zod` internals. + * * @param value - Any value * @returns True when the value is a Zod 4 schema instance */ @@ -400,6 +402,8 @@ function isZod4Type(value: unknown): value is z.ZodType { } /** + * Detects a Zod 3 schema by its `_def` internals without `_zod`. + * * @param value - Any value * @returns True when the value is a Zod 3 schema instance (has `_def` but not `_zod`) */ diff --git a/src/http/FastifyTransport.ts b/src/http/FastifyTransport.ts index 1f24059..6d73e49 100644 --- a/src/http/FastifyTransport.ts +++ b/src/http/FastifyTransport.ts @@ -400,7 +400,7 @@ export class FastifyTransport { /** * Closes all active sessions and clears the session map so the server is * free to accept a new connection. Required because the SDK throws - * "Already connected" when `connect()` is called while a transport is + * `AlreadyConnected` when `connect()` is called while a transport is * still attached. Handles unclean client disconnects followed by re-init. * * The map is cleared before closing so that `onclose` handlers fired by diff --git a/src/permissions/PermissionAwareFastifyTransport.ts b/src/permissions/PermissionAwareFastifyTransport.ts index e8fe86b..b33ab20 100644 --- a/src/permissions/PermissionAwareFastifyTransport.ts +++ b/src/permissions/PermissionAwareFastifyTransport.ts @@ -181,7 +181,7 @@ export class PermissionAwareFastifyTransport { /** * Closes all active sessions and clears the session map so the server is * free to accept a new connection. Required because the SDK throws - * "Already connected" when `connect()` is called while a transport is + * `AlreadyConnected` when `connect()` is called while a transport is * still attached. Handles unclean client disconnects followed by re-init. * * The map is cleared before closing so that `onclose` handlers fired by diff --git a/tests/dependencies.test.ts b/tests/dependencies.test.ts index a54bd50..d27297b 100644 --- a/tests/dependencies.test.ts +++ b/tests/dependencies.test.ts @@ -22,7 +22,7 @@ function copiesOf(name: string): string[] { } describe("dependency tree", () => { - it.each(["zod", "@modelcontextprotocol/server", "@modelcontextprotocol/core"])( + it.each(["zod", "@modelcontextprotocol/server"])( "has exactly one copy of %s", (name) => { expect(copiesOf(name)).toEqual([`node_modules/${name}`]); diff --git a/tests/dynamicToolManager.test.ts b/tests/dynamicToolManager.test.ts index 8b4989d..9261ad4 100644 --- a/tests/dynamicToolManager.test.ts +++ b/tests/dynamicToolManager.test.ts @@ -341,7 +341,7 @@ describe("DynamicToolManager with a real McpServer", () => { }); describe("DynamicToolManager input schema handling (Zod 4)", () => { - async function enable(inputSchema: unknown) { + async function enable(inputSchema: unknown, precedingTools: unknown[] = []) { const server = new McpServer({ name: "t", version: "1.0.0" }); const resolver = new ModuleResolver({ catalog: { @@ -349,6 +349,7 @@ describe("DynamicToolManager input schema handling (Zod 4)", () => { name: "Core", description: "", tools: [ + ...precedingTools, { name: "echo", description: "Echo", @@ -397,10 +398,16 @@ describe("DynamicToolManager input schema handling (Zod 4)", () => { expect((res.content as any)[0].text).toBe("none"); }); - it("refuses a Zod 3 schema with a clear error, and registers nothing", async () => { + it("refuses a Zod 3 schema with a clear error, and rolls back the toolset's earlier tools", async () => { // A Zod 3 type has `_def` but no `_zod`. const zod3Like = { _def: { typeName: "ZodString" }, parse: () => "" }; - const { result, client } = await enable({ text: zod3Like }); + const first = { + name: "first", + description: "Registers before the bad tool", + inputSchema: {}, + handler: async () => ({ content: [{ type: "text", text: "ok" }] }), + }; + const { result, client } = await enable({ text: zod3Like }, [first]); expect(result.success).toBe(false); expect(result.code).toBe("E_VALIDATION"); expect(result.message).toMatch(/Tool 'echo' uses a Zod 3 schema.*Zod 4/); diff --git a/tests/e2e/multiClient.e2e.test.ts b/tests/e2e/multiClient.e2e.test.ts index a0a21d2..e46b941 100644 --- a/tests/e2e/multiClient.e2e.test.ts +++ b/tests/e2e/multiClient.e2e.test.ts @@ -63,8 +63,14 @@ async function metaCall(c: Connected, tool: string, name: string) { ); } -/** Lets pending SSE notifications reach the clients. */ -const settle = () => new Promise((resolve) => setTimeout(resolve, 100)); +/** Polls until `condition` holds, so tests do not depend on a fixed delay. */ +async function waitFor(condition: () => boolean, timeoutMs = 3000): Promise { + const deadline = Date.now() + timeoutMs; + while (!condition()) { + if (Date.now() > deadline) throw new Error("Timed out waiting for condition"); + await new Promise((resolve) => setTimeout(resolve, 10)); + } +} describe("multiple concurrent clients keep their own toolset state", () => { describe("DYNAMIC mode", () => { @@ -143,14 +149,14 @@ describe("multiple concurrent clients keep their own toolset state", () => { it("sends tools/list_changed only to the client whose state changed", async () => { const a = await connect(port, "multi-a3"); const b = await connect(port, "multi-b3"); - await settle(); const bBefore = b.listChangedCount(); const aBefore = a.listChangedCount(); await metaCall(a, "enable_toolset", "core"); - await settle(); + // Once A has been notified, B would have been too if the change leaked. + await waitFor(() => a.listChangedCount() > aBefore); + await toolNames(b); - expect(a.listChangedCount()).toBeGreaterThan(aBefore); expect(b.listChangedCount()).toBe(bBefore); await a.client.close(); diff --git a/tests/smoke-e2e/permission-config-server-demo.ts b/tests/smoke-e2e/permission-config-server-demo.ts index aad3142..e2a02ee 100644 --- a/tests/smoke-e2e/permission-config-server-demo.ts +++ b/tests/smoke-e2e/permission-config-server-demo.ts @@ -84,7 +84,7 @@ const createServer = () => new McpServer({ name: "toolception-permission-config-demo", version: "0.1.0", - }, { capabilities: { tools: {} } }) // STATIC mode, no listChanged; + }, { capabilities: { tools: {} } }); // STATIC mode, no listChanged /** * Static permission map for known clients. diff --git a/tests/smoke-e2e/permission-header-server-demo.ts b/tests/smoke-e2e/permission-header-server-demo.ts index 3fb52f3..86a4682 100644 --- a/tests/smoke-e2e/permission-header-server-demo.ts +++ b/tests/smoke-e2e/permission-header-server-demo.ts @@ -86,7 +86,7 @@ const createServer = () => new McpServer({ name: "toolception-permission-header-demo", version: "0.1.0", - }, { capabilities: { tools: {} } }) // STATIC mode, no listChanged; + }, { capabilities: { tools: {} } }); // STATIC mode, no listChanged const { start, close } = await createPermissionBasedMcpServer({ catalog, From 231a26c4c81865941cd22fc0c06530b1b4e189bb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 12:03:12 +0000 Subject: [PATCH 3/3] test: open both SSE streams before the list_changed isolation check Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01VmUXGSYMuTcM7fquxzRook --- tests/e2e/multiClient.e2e.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/e2e/multiClient.e2e.test.ts b/tests/e2e/multiClient.e2e.test.ts index e46b941..02b0f14 100644 --- a/tests/e2e/multiClient.e2e.test.ts +++ b/tests/e2e/multiClient.e2e.test.ts @@ -149,6 +149,9 @@ describe("multiple concurrent clients keep their own toolset state", () => { it("sends tools/list_changed only to the client whose state changed", async () => { const a = await connect(port, "multi-a3"); const b = await connect(port, "multi-b3"); + // A round trip on each client first, so both standalone SSE streams are open. + await toolNames(a); + await toolNames(b); const bBefore = b.listChangedCount(); const aBefore = a.listChangedCount();