diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 646ab9d..3a21036 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "25.3.0" + node-version: "26" cache: "npm" registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ca520a..26c3e00 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "25.3.0" + node-version: "26" cache: "npm" registry-url: "https://registry.npmjs.org" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aca200..908cef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,21 +4,29 @@ 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] +## [0.9.0] ### 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. +- 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. ([#35](https://github.com/code-rabi/toolception/pull/35)) - **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. +- 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. ([#35](https://github.com/code-rabi/toolception/pull/35)) +- `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. ([#35](https://github.com/code-rabi/toolception/pull/35)) +- A tool whose `inputSchema` is a plain JSON Schema object (`type: "object"`) is now registered with the SDK's `fromJsonSchema()`: clients see its real properties and arguments are validated against it before your handler runs. Until now such a tool registered as an open object, so its parameters were not advertised and any arguments reached the handler. Invalid arguments now get an error result and the handler is not called. ([#38](https://github.com/code-rabi/toolception/pull/38)) + - **Migration:** if a handler relied on receiving arguments its schema forbids (a missing `required` property, a wrong type, or extra properties under `additionalProperties: false`), loosen the schema or fix the caller. + - A JSON Schema that cannot be compiled (unknown type name, unresolved `$ref`, unsupported `$schema` dialect) now fails the toolset's enable with an `E_VALIDATION` error that names the tool. So does an `inputSchema` that is neither a Zod 4 shape nor a JSON Schema object (the root `type` must be the string `"object"`, so `type: ["object", "null"]` is refused; this includes a shape that mixes Zod and non-Zod values), which used to register as an open object. + - Each client's `DynamicToolManager` compiles JSON Schemas with its own validator, so compiled schemas are not shared between clients and are freed with the client. The root `$id` of a tool's schema is not used or advertised (the validator caches by root `$id`, which would give two tools with the same `$id` the first one's validator); reference the root with `#`, not its `$id` URL. ### 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. +- 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. `vite-plugin-dts` 5 and a dev-only `minimatch` override clear the last dev-only audit findings; none of this ships in the package. ([#35](https://github.com/code-rabi/toolception/pull/35), [#36](https://github.com/code-rabi/toolception/pull/36)) + +### Changed + +- CI and the release workflow run on Node 26, because vitest 5 does not support Node 25. `engines` stays `>=25.3.0`, so consumers are unaffected. ([#37](https://github.com/code-rabi/toolception/pull/37)) ## [0.8.0] diff --git a/CLAUDE.md b/CLAUDE.md index 120b76c..ed6fa69 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,4 +63,4 @@ Tests use Vitest with in-memory mocks. Key patterns: - Vite for bundling (`vite.config.ts`) - vite-plugin-dts for TypeScript declarations - ESM-only output (`"type": "module"`) -- Node.js >= 25.3.0 required +- Node.js >= 25.3.0 required (`engines`). CI and the release workflow run on Node 26, because vitest 5 supports `^22.12 || ^24 || >=26` and not Node 25 diff --git a/README.md b/README.md index 9013a5d..2b3c397 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ const catalog = { ### Step 4: Define a tool -`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. +`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 (`type: "object"`) also works: toolception registers it with the SDK's `fromJsonSchema()`, so clients see its properties and arguments are validated against it before your handler runs. A JSON Schema that does not compile (unknown type name, unresolved `$ref`, unsupported `$schema` dialect) fails the toolset's enable with an `E_VALIDATION` error that names the tool. ```ts import { z } from "zod"; diff --git a/package-lock.json b/package-lock.json index 1bd6e1c..1fb7579 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "toolception", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "toolception", - "version": "0.8.0", + "version": "0.9.0", "license": "Apache-2.0", "dependencies": { "@fastify/cors": "^10.0.1", @@ -23,7 +23,7 @@ "tsx": "^4.19.0", "typescript": "^5.9.2", "vite": "^7.2.4", - "vite-plugin-dts": "^4.5.4", + "vite-plugin-dts": "^5.1.2", "vitest": "^5.0.3" }, "engines": { @@ -675,6 +675,37 @@ "hono": "^4 || ^5.0.0-0" } }, + "node_modules/@isaacs/nonrecursive-glob-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.0.3.tgz", + "integrity": "sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "peer": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -709,6 +740,8 @@ "integrity": "sha512-1FJ/Z30xLL3pWn6oYSJCjGsnlEWkWivwmVeCB7YrRPe+47J7H54NVR+m/1PIL1RYsh/mZsxm/FwirreD72G76g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@microsoft/api-extractor-model": "7.33.16", "@microsoft/tsdoc": "~0.17.0", @@ -737,6 +770,8 @@ "integrity": "sha512-MO61eHRI3DDieE9pxNo5xc096NGUQAIKzEPW6IhNNGU9K4EIhRSYiXfroDZX4joNkrWBMzAMp3WhbPGbHG5zHQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@microsoft/tsdoc": "~0.17.0", "@microsoft/tsdoc-config": "~0.18.2", @@ -746,51 +781,14 @@ "node": ">=20.9.0" } }, - "node_modules/@microsoft/api-extractor/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/brace-expansion": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", - "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/minimatch": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", - "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "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", + "optional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -804,7 +802,9 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.17.1.tgz", "integrity": "sha512-VHyYyyoH63jFoO3shpw9Zfijk5v/9p/VrClW35z0/q6ooK7sSmxpzOkS4iZPxs3vO4XtBW4H38HRAKkAMIK7DA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@microsoft/tsdoc-config": { "version": "0.18.3", @@ -812,6 +812,8 @@ "integrity": "sha512-0I0rGXo1eJziw3EEErkDS9p/0RzBcyOiV+zbSDqOZTeURmhtut+WIvpih0LM5g+ZpZ9SyFxDfkhAcl/r8e2zGw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@microsoft/tsdoc": "0.17.1", "ajv": "~8.18.0", @@ -886,9 +888,9 @@ "license": "MIT" }, "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.5.1.tgz", + "integrity": "sha512-P9ZvnfouRu4MSC+ilERaw6FmZs8zqKhpHxEs4aGDGWQAMzQGXnhvCSvoNwokfNvdt7t3Y5Y4p6gStQgezeIqug==", "dev": true, "license": "MIT", "dependencies": { @@ -1271,6 +1273,8 @@ "integrity": "sha512-76QGqWRVMtd/KZqhfSOMBgDvxIUsYPhUBhLSorGdn/wwkuESoJ2/cUI3KmfMKrZrE72o8Lau4P8kCBH/omI17Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ajv": "~8.20.0", "ajv-draft-04": "~1.0.0", @@ -1299,6 +1303,8 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1316,6 +1322,8 @@ "integrity": "sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "peerDependencies": { "@types/node": "*" }, @@ -1331,6 +1339,8 @@ "integrity": "sha512-SHZdzc9b2ptUb5uvRWALtAceD/QKZ4UAnZjcaKviTMPmHBnW14gs7XDocn6sDv2EqgnxxFPFkpV7/I3cUijCbw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "jju": "~1.4.0", "resolve": "~1.22.1" @@ -1345,6 +1355,8 @@ "integrity": "sha512-VJAq8ZVgnEtXOC+FnBL3YdP5YNBibT9JvRvEuXsqi0ENZ+Ha0uR9CaK2c+4y8hdk/n6Y2SJ6+AZYDfSQmqupWg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@rushstack/node-core-library": "5.25.2", "@rushstack/problem-matcher": "0.2.1", @@ -1368,6 +1380,8 @@ "integrity": "sha512-acP68tGx/DJEIQgTqj/xStBRaghN7axV+uAjNasDjVXgHgHaLQ1f16/bbXtw6mNLjURhTORD4fdfbOZM7kyWcw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@rushstack/terminal": "0.24.8", "@types/argparse": "1.0.38", @@ -1383,7 +1397,9 @@ "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/chai": { "version": "5.2.3", @@ -1531,95 +1547,57 @@ } }, "node_modules/@volar/language-core": { - "version": "2.4.27", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.27.tgz", - "integrity": "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==", + "version": "2.4.30", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.30.tgz", + "integrity": "sha512-73fjwEV8wat6aOZ6U/y/hqtE30ocrIWECg67b0txGczOMNw+MffLmvJ5fnFfN9drVUEYZzLdaNo6VAy6G6avMw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.27" + "@volar/source-map": "2.4.30" } }, - "node_modules/@volar/source-map": { - "version": "2.4.27", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.27.tgz", - "integrity": "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@volar/typescript": { - "version": "2.4.27", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.27.tgz", - "integrity": "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==", + "node_modules/@volar/language-service": { + "version": "2.4.30", + "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.30.tgz", + "integrity": "sha512-w/xDsASiqC71ggCOm7sTDXDNk/l72XEptodeRC68tgZ0e5QM4hM3zAqBGC7mBctvhrpYwKfMG+t9xcUo2hEFtA==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.27", - "path-browserify": "^1.0.1", + "@volar/language-core": "2.4.30", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "<7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@vue/compiler-core": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", - "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.26", - "entities": "^7.0.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-core/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "node_modules/@volar/source-map": { + "version": "2.4.30", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.30.tgz", + "integrity": "sha512-zym1ob/KwDDyN+0jGVtxwbxJSRRnUvv5HspCtz3yEfVVdCG4/dzxJktMGPAgemqRuVs81O0o/y0q3qOUErR8kg==", "dev": true, "license": "MIT" }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", - "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.26", - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/compiler-vue2": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", - "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", - "dev": true, - "license": "MIT", - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/@vue/language-core": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.0.tgz", - "integrity": "sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==", + "node_modules/@volar/typescript": { + "version": "2.4.30", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.30.tgz", + "integrity": "sha512-/DgA5tgYFPfrLFuSjIMXMGHk1LQv2+P8eny6+BhGNzFJELWIW7UZlc56ZLIRFhLsPpMfIFxM8/HvOPBggrjTZg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "~2.4.11", - "@vue/compiler-dom": "^3.5.0", - "@vue/compiler-vue2": "^2.7.16", - "@vue/shared": "^3.5.0", - "alien-signals": "^0.4.9", - "minimatch": "^9.0.3", - "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1" + "@volar/language-core": "2.4.30", + "@volar/language-service": "2.4.30", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" }, "peerDependencies": { - "typescript": "*" + "typescript": "<7" }, "peerDependenciesMeta": { "typescript": { @@ -1627,13 +1605,6 @@ } } }, - "node_modules/@vue/shared": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", - "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", - "dev": true, - "license": "MIT" - }, "node_modules/abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -1641,9 +1612,9 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "dev": true, "license": "MIT", "bin": { @@ -1675,6 +1646,8 @@ "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "peerDependencies": { "ajv": "^8.5.0" }, @@ -1701,19 +1674,14 @@ } } }, - "node_modules/alien-signals": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.4.14.tgz", - "integrity": "sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -1760,20 +1728,30 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/brace-expansion": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/chai": { @@ -1794,9 +1772,9 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.2.tgz", + "integrity": "sha512-ZUAKhKlkCOrIGKc8cj7zek9s9z1mveHif/25nUshI2BzCRzF7KH7KHa+Lnh2ubq+e1tIlaFuP763mzW4qVoehg==", "dev": true, "license": "MIT" }, @@ -1815,13 +1793,6 @@ "node": ">= 8" } }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true, - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1855,29 +1826,20 @@ "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=0.3.1" } }, - "node_modules/entities": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", - "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "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", + "optional": true, + "peer": true, "engines": { "node": ">= 0.4" } @@ -1975,9 +1937,9 @@ } }, "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.2.0.tgz", + "integrity": "sha512-h1pQPR9XoeSMdntRNCjyTznaume4UpfXrULBnlkZ2CUM/G5eSa+pBByGldO5uR8dxbPC+OuJXsPPNf+JZ81ADg==", "dev": true, "license": "MIT" }, @@ -2178,6 +2140,8 @@ "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2208,6 +2172,8 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2225,12 +2191,21 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/has-flag": { "version": "4.0.0", @@ -2238,6 +2213,8 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -2248,6 +2225,8 @@ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -2255,16 +2234,6 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/hono": { "version": "4.13.16", "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.16.tgz", @@ -2280,6 +2249,8 @@ "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -2299,6 +2270,8 @@ "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "hasown": "^2.0.4" }, @@ -2321,7 +2294,9 @@ "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/jose": { "version": "6.2.15", @@ -2371,6 +2346,8 @@ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -2436,9 +2413,9 @@ "license": "MIT" }, "node_modules/local-pkg": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", - "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2476,32 +2453,35 @@ } }, "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.4.tgz", + "integrity": "sha512-/UJ1dEoVQTjPNDh6SGlUo+ueLMmoHTas4q+8yaAR/5t6nLAIiBDJtoYUh20IYleix7ehiQkgczb84PE3uzHkxg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", + "optional": true, + "peer": true, "dependencies": { - "brace-expansion": "^2.0.2" + "@isaacs/nonrecursive-glob-match": "^1.0.3", + "brace-expansion": "^5.0.12" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", - "ufo": "^1.6.1" + "ufo": "^1.6.3" } }, "node_modules/mlly/node_modules/confbox": { @@ -2539,13 +2519,6 @@ "dev": true, "license": "MIT" }, - "node_modules/muggle-string": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", - "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", - "dev": true, - "license": "MIT" - }, "node_modules/nanoid": { "version": "3.3.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", @@ -2616,7 +2589,9 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/pathe": { "version": "2.0.3", @@ -2693,14 +2668,14 @@ } }, "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.4.0.tgz", + "integrity": "sha512-07Fys7iVx0X6TcviMOgogM1Lk33ZyqOezMnU79S+kffWqwolG/5W9V8EgQSeYCIfFTYVgssRbnH630Oa2+soBA==", "dev": true, "license": "MIT", "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", + "confbox": "^0.3.1", + "exsolve": "^1.1.3", "pathe": "^2.0.3" } }, @@ -2796,6 +2771,8 @@ "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.17.0", @@ -2995,6 +2972,8 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3023,7 +3002,9 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true, + "peer": true }, "node_modules/std-env": { "version": "4.3.0", @@ -3038,6 +3019,8 @@ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=0.6.19" } @@ -3048,6 +3031,8 @@ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3064,6 +3049,8 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.4" }, @@ -3171,9 +3158,9 @@ } }, "node_modules/ufo": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.2.tgz", - "integrity": "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "dev": true, "license": "MIT" }, @@ -3190,10 +3177,83 @@ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 10.0.0" } }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-dts": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.1.2.tgz", + "integrity": "sha512-RDqy90XUmFj6rz4U5YoTMdYdaCJe2ZnQZaWIjkWS4952Gv0zBTQyqWtlGgVMPYHa982dURvyoc+sUOFIcDFv+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "@volar/typescript": "^2.4.26", + "compare-versions": "^6.1.1", + "debug": "^4.4.0", + "glob-to-regexp": "0.4.1", + "kolorist": "^1.8.0", + "local-pkg": "^1.1.1", + "magic-string": "^0.30.17", + "unplugin": "^2.3.2" + }, + "peerDependencies": { + "@microsoft/api-extractor": ">=7", + "@rspack/core": "^1", + "@vue/language-core": "^3.1.5", + "esbuild": "*", + "rolldown": "*", + "rollup": ">=3", + "typescript": ">=4", + "vite": ">=3", + "webpack": "^4 || ^5" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "@vue/language-core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, "node_modules/vite": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", @@ -3270,27 +3330,26 @@ } }, "node_modules/vite-plugin-dts": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-4.5.4.tgz", - "integrity": "sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-5.1.2.tgz", + "integrity": "sha512-1bXUz836sWq6ewEMQre3ZtNFVfZk/92DppgCCKP8tWx/NWwV2wHuXlRHb2TztHjbikPxvyAGLGu7hVh94Plwww==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor": "^7.50.1", - "@rollup/pluginutils": "^5.1.4", - "@volar/typescript": "^2.4.11", - "@vue/language-core": "2.2.0", - "compare-versions": "^6.1.1", - "debug": "^4.4.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.0.0", - "magic-string": "^0.30.17" + "unplugin-dts": "1.1.2" }, "peerDependencies": { - "typescript": "*", - "vite": "*" + "@microsoft/api-extractor": ">=7", + "rollup": ">=3", + "vite": ">=3" }, "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "rollup": { + "optional": true + }, "vite": { "optional": true } @@ -3389,10 +3448,52 @@ "@jridgewell/sourcemap-codec": "^1.6.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-10.0.0.tgz", + "integrity": "sha512-USSfxg0vDIpTQqlQBGxF3mBARyoS8cU7LHLpbYNh6dOJt32dlskxPWB0Irrh5TllTmyf7oNLAvqDqzklbwwnpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.5.tgz", + "integrity": "sha512-FW4fBMzZ99N8xeob4py0l050wgCoRgTpEq5Xcsb9lM1bc29UKGR/wSV+4knaqFxssFHajy7OzNiKP/m3U1ZEuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "10.0.0", + "vscode-languageserver-types": "3.18.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.16.tgz", + "integrity": "sha512-qeNEuOi9a3qkqRf7Gfez2WL2VwlBTLCNekHBGO9kxPVUp9XuY15yfTJ9jwvOwKI0I2MhUUfZq95NSp+LK1CLQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.5.tgz", + "integrity": "sha512-kPd6+JQNz3HMJvjKlqUxERSlZHAntl/aGpsk7ImwIh0ewDKDcjgAEyVSYciPiOJD3ftbK9IrfiqUeTZLSbCizw==", + "dev": true, + "license": "MIT" + }, "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 7f8a88e..7c93c17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "toolception", - "version": "0.8.0", + "version": "0.9.0", "private": false, "type": "module", "main": "dist/index.js", @@ -38,7 +38,7 @@ "tsx": "^4.19.0", "typescript": "^5.9.2", "vite": "^7.2.4", - "vite-plugin-dts": "^4.5.4", + "vite-plugin-dts": "^5.1.2", "vitest": "^5.0.3" }, "engines": { @@ -79,5 +79,10 @@ }, "bugs": { "url": "https://github.com/code-rabi/toolception/issues" + }, + "overrides": { + "@microsoft/api-extractor": { + "minimatch": "^11.0.1" + } } } diff --git a/src/core/AGENTS.md b/src/core/AGENTS.md index 5eb3de9..ec47ab9 100644 --- a/src/core/AGENTS.md +++ b/src/core/AGENTS.md @@ -18,8 +18,9 @@ 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. **`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. +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; a JSON Schema object with root `type: "object"` goes through the SDK's `fromJsonSchema()`, so clients see the declared schema and arguments are validated before the handler runs; a missing or non-object schema registers as `z.looseObject({})`; anything else (a root `type` that is not the string `"object"`, which includes a shape mixing Zod and non-Zod values; a JSON Schema that fails to compile) throws `ToolingError` `E_VALIDATION` at registration, so the enable fails. The root `$id` is dropped before compiling (the validator caches by `$id`, so two tools sharing one would share the first one's validator). 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. +9. **JSON Schema validators are per manager** — Each `DynamicToolManager` (one per client) owns its own `AjvJsonSchemaValidator`. Ajv keeps every compiled schema for the validator's lifetime, so the SDK's process-wide default validator would hold all clients' schemas until exit; a per-manager one is freed with the client and shares nothing. Ajv's cache is never evicted when a toolset is disabled, so a client that toggles a loader-backed toolset, or one whose schema has a root `$id` (copied on each enable), many times grows its own cache until the session ends; that growth is bounded by the client's lifetime. Do not switch to `fromJsonSchema(schema)` without a validator argument. ## Enable Toolset Flow @@ -34,7 +35,7 @@ enableToolset(name) ## SDK registration -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. +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, or wraps a JSON Schema with `fromJsonSchema()`, 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 525025d..9ec0a2e 100644 --- a/src/core/DynamicToolManager.ts +++ b/src/core/DynamicToolManager.ts @@ -1,4 +1,12 @@ -import type { McpServer, RegisteredTool } from "@modelcontextprotocol/server"; +import { + fromJsonSchema, + type JsonSchemaType, + type McpServer, + type RegisteredTool, + type StandardSchemaWithJSON, + type jsonSchemaValidator, +} from "@modelcontextprotocol/server"; +import { AjvJsonSchemaValidator } from "@modelcontextprotocol/server/validators/ajv"; import { z } from "zod"; import type { ExposurePolicy, @@ -22,6 +30,13 @@ export class DynamicToolManager { private readonly activeToolsets = new Set(); /** SDK handles of each toolset's registered tools, used to unregister them. */ private readonly toolHandles = new Map(); + /** + * Compiles the JSON Schemas of this manager's tools. It is deliberately per + * manager (so per client): Ajv keeps every compiled schema for the validator's + * lifetime, so the SDK's process-wide default validator would hold all clients' + * schemas until exit. Here they are freed with the client, and nothing is shared. + */ + private readonly schemaValidator = new AjvJsonSchemaValidator(); constructor(options: DynamicToolManagerOptions) { this.server = options.server; @@ -247,7 +262,11 @@ export class DynamicToolManager { const hasAnnotations = tool.annotations && Object.keys(tool.annotations).length > 0; - const inputSchema = toRegistrableInputSchema(tool.name, tool.inputSchema); + const inputSchema = toRegistrableInputSchema( + tool.name, + tool.inputSchema, + this.schemaValidator + ); const handle = this.server.registerTool( tool.name, @@ -389,7 +408,10 @@ export class DynamicToolManager { } /** The schema type handed to `registerTool`; arguments arrive as a plain record. */ -type InputSchema = z.ZodType>; +type InputSchema = StandardSchemaWithJSON< + Record, + Record +>; /** * Detects a Zod 4 schema by its `_zod` internals. @@ -417,26 +439,38 @@ function isZod3Type(value: unknown): boolean { } /** - * Turns a tool's `inputSchema` into the Zod 4 object schema that - * `McpServer.registerTool` expects. + * Turns a tool's `inputSchema` into the 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. + * - A plain JSON Schema object (`type: "object"`) goes through the SDK's + * `fromJsonSchema()`: clients see its real properties and arguments are + * validated against it before the handler runs. + * The root `$id` is dropped before compiling (so it is not advertised either): the + * validator caches compiled schemas by `$id`, which would give two tools with the + * same `$id` the first tool's validator. Reference the root with `#`, not its URL. + * - A missing or non-object schema registers as an open object, so arguments still + * reach the handler. * - * @param toolName - The tool name, used in the error message + * @param toolName - The tool name, used in error messages * @param schema - The tool's inputSchema + * @param validator - Compiles JSON Schema tools; scoped to one client by the caller * @returns A schema to pass to `registerTool` - * @throws ToolingError `E_VALIDATION` when the schema or a shape value is a Zod 3 schema + * @throws ToolingError `E_VALIDATION` when the schema or a shape value is a Zod 3 schema, + * when the schema is neither a Zod 4 shape nor a JSON Schema object, or when the JSON + * Schema cannot be compiled (bad keyword, unresolved `$ref`, unsupported dialect) */ function toRegistrableInputSchema( toolName: string, - schema: unknown + schema: unknown, + validator: jsonSchemaValidator ): InputSchema { - if (typeof schema !== "object" || schema === null) return z.looseObject({}); + if (typeof schema !== "object" || schema === null) { + return z.looseObject({}) as InputSchema; + } const values = isZod4Type(schema) || isZod3Type(schema) ? [schema] : Object.values(schema); @@ -452,5 +486,28 @@ function toRegistrableInputSchema( if (values.every(isZod4Type)) { return z.object(schema as Record) as InputSchema; } - return z.looseObject({}); + if ((schema as { type?: unknown }).type !== "object") { + throw new ToolingError( + `Tool '${toolName}' has an inputSchema that is neither a Zod 4 shape nor a JSON Schema object (type: "object").`, + "E_VALIDATION" + ); + } + // Copy only when there is a root `$id` to drop: Ajv caches by object identity, + // so a schema without one is passed as is and a re-enable reuses its compiled + // form. A copy is a new cache key, bounded by the client's lifetime. + let compilable = schema as JsonSchemaType; + if (typeof compilable.$id === "string") { + compilable = { ...compilable }; + delete compilable.$id; + } + try { + return fromJsonSchema>(compilable, validator); + } catch (error) { + throw new ToolingError( + `Tool '${toolName}' has an invalid JSON Schema inputSchema: ${ + error instanceof Error ? error.message : String(error) + }`, + "E_VALIDATION" + ); + } } diff --git a/src/types/AGENTS.md b/src/types/AGENTS.md index 4c0a3c0..6f55f35 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 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) +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`. A plain JSON Schema object (`type: "object"`) is registered through the SDK's `fromJsonSchema()`, so it is advertised and its arguments are validated. A schema that does not compile, or has no root `type: "object"`, fails the enable with `E_VALIDATION` ## Anti-patterns diff --git a/src/types/index.ts b/src/types/index.ts index 74cdcd2..2209dba 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,8 +6,10 @@ export type McpToolDefinition = { /** * 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. + * a Zod 3 schema is refused (`E_VALIDATION`). A plain JSON Schema object + * (`type: "object"`) is registered with the SDK's `fromJsonSchema()`: clients see its + * properties and arguments are validated before the handler runs. A JSON Schema that + * cannot be compiled, or any other shape, fails the toolset's enable (`E_VALIDATION`). */ inputSchema: Record; handler: (args: any) => Promise | any; diff --git a/tests/dynamicToolManager.test.ts b/tests/dynamicToolManager.test.ts index 9261ad4..cf0008f 100644 --- a/tests/dynamicToolManager.test.ts +++ b/tests/dynamicToolManager.test.ts @@ -322,7 +322,7 @@ describe("DynamicToolManager with a real McpServer", () => { expect((res.content as any)[0].text).toBe("AAPL"); }); - it("registers a plain JSON Schema inputSchema as a passthrough object", async () => { + it("registers a plain JSON Schema inputSchema and hands valid arguments to the handler", async () => { const { result, client } = await connect([ { name: "price", @@ -421,6 +421,240 @@ describe("DynamicToolManager input schema handling (Zod 4)", () => { }); }); +describe("DynamicToolManager JSON Schema inputSchema", () => { + const symbolSchema = { + type: "object", + properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1 } }, + required: ["symbol"], + additionalProperties: false, + }; + + function managerFor(inputSchema: unknown, handler = vi.fn(async (args: any) => ({ + content: [{ type: "text", text: String(args.symbol ?? "none") }], + }))) { + const server = new McpServer({ name: "t", version: "1.0.0" }); + const resolver = new ModuleResolver({ + catalog: { + core: { + name: "Core", + description: "", + tools: [{ name: "price", description: "Price", inputSchema, handler }], + }, + } as any, + }); + const manager = new DynamicToolManager({ + server, + resolver, + toolRegistry: new ToolRegistry({ namespaceWithToolset: false }), + }); + return { server, manager, handler }; + } + + async function connectClient(server: McpServer) { + const [serverSide, clientSide] = InMemoryTransport.createLinkedPair(); + await server.connect(serverSide); + const client = new Client({ name: "c", version: "1.0.0" }); + await client.connect(clientSide); + return client; + } + + it("advertises the declared properties and required list", async () => { + const { server, manager } = managerFor(symbolSchema); + expect((await manager.enableToolset("core")).success).toBe(true); + const client = await connectClient(server); + const { tools } = await client.listTools(); + expect(tools[0].inputSchema).toMatchObject({ + type: "object", + properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1 } }, + required: ["symbol"], + additionalProperties: false, + }); + }); + + it.each([ + ["a missing required property", {}, /required property 'symbol'/], + ["a wrong type", { symbol: 42 }, /symbol must be string/], + ["a violated constraint", { symbol: "AAPL", limit: 0 }, /limit must be >= 1/], + ["an extra property the schema forbids", { symbol: "AAPL", extra: true }, /additional properties/], + ])("rejects %s without calling the handler", async (_label, args, detail) => { + const { server, manager, handler } = managerFor(symbolSchema); + await manager.enableToolset("core"); + const client = await connectClient(server); + const res = await client.callTool({ name: "price", arguments: args }); + expect(res.isError).toBe(true); + expect((res.content as any)[0].text).toMatch(detail); + expect(handler).not.toHaveBeenCalled(); + }); + + it("passes valid arguments through unchanged", async () => { + const { server, manager, handler } = managerFor(symbolSchema); + await manager.enableToolset("core"); + const client = await connectClient(server); + const res = await client.callTool({ + name: "price", + arguments: { symbol: "AAPL", limit: 3 }, + }); + expect(res.isError).toBeFalsy(); + expect(handler).toHaveBeenCalledWith({ symbol: "AAPL", limit: 3 }); + }); + + it("registers a tool without an inputSchema as an open object", async () => { + const { server, manager } = managerFor(undefined); + expect((await manager.enableToolset("core")).success).toBe(true); + const client = await connectClient(server); + const res = await client.callTool({ name: "price", arguments: { symbol: "X" } }); + expect((res.content as any)[0].text).toBe("X"); + }); + + it.each([ + ["an unknown type name", { type: "object", properties: { a: { type: "strng" } } }], + ["an unresolved $ref", { type: "object", properties: { a: { $ref: "#/$defs/missing" } } }], + ["an unsupported $schema dialect", { $schema: "http://example.com/other", type: "object" }], + ])("fails the enable with E_VALIDATION on %s, and registers nothing", async (_label, schema) => { + const { server, manager } = managerFor(schema); + const result = await manager.enableToolset("core"); + expect(result.success).toBe(false); + expect(result.code).toBe("E_VALIDATION"); + expect(result.message).toMatch(/Tool 'price' has an invalid JSON Schema inputSchema/); + expect(manager.isActive("core")).toBe(false); + const client = await connectClient(server); + expect((await client.listTools()).tools).toEqual([]); + }); + + it.each([ + ["a JSON Schema whose root is not an object", { type: "string" }], + ["a JSON Schema without a root type", { properties: { a: { type: "string" } } }], + ["a shape that mixes Zod and non-Zod values", { a: z.string(), b: 5 }], + ["a root type array", { type: ["object", "null"] }], + ])("fails the enable with E_VALIDATION on %s", async (_label, schema) => { + const { manager } = managerFor(schema); + const result = await manager.enableToolset("core"); + expect(result.success).toBe(false); + expect(result.code).toBe("E_VALIDATION"); + expect(result.message).toMatch(/neither a Zod 4 shape nor a JSON Schema object/); + }); + + it("validates two managers against their own schemas, even for identical nested $id values", async () => { + const schemaFor = (type: "string" | "number") => ({ + type: "object", + properties: { value: { $ref: "#/$defs/value" } }, + required: ["value"], + $defs: { value: { $id: "https://example.com/value", type } }, + }); + const a = managerFor(schemaFor("string")); + const b = managerFor(schemaFor("number")); + expect((await a.manager.enableToolset("core")).success).toBe(true); + expect((await b.manager.enableToolset("core")).success).toBe(true); + const clientA = await connectClient(a.server); + const clientB = await connectClient(b.server); + + const aString = await clientA.callTool({ name: "price", arguments: { value: "x" } }); + const aNumber = await clientA.callTool({ name: "price", arguments: { value: 1 } }); + const bString = await clientB.callTool({ name: "price", arguments: { value: "x" } }); + const bNumber = await clientB.callTool({ name: "price", arguments: { value: 1 } }); + + expect(aString.isError).toBeFalsy(); + expect(aNumber.isError).toBe(true); + expect(bString.isError).toBe(true); + expect(bNumber.isError).toBeFalsy(); + }); + + it("validates two tools of one manager against their own schema when both declare the same root $id", async () => { + const server = new McpServer({ name: "t", version: "1.0.0" }); + const echo = (args: any) => ({ content: [{ type: "text", text: String(args.value) }] }); + const schemaFor = (type: "string" | "number") => ({ + $id: "https://example.com/args", + type: "object", + properties: { value: { type } }, + required: ["value"], + }); + const resolver = new ModuleResolver({ + catalog: { + core: { + name: "Core", + description: "", + tools: [ + { name: "text", description: "Text", inputSchema: schemaFor("string"), handler: echo }, + { name: "count", description: "Count", inputSchema: schemaFor("number"), handler: echo }, + ], + }, + } as any, + }); + const manager = new DynamicToolManager({ + server, + resolver, + toolRegistry: new ToolRegistry({ namespaceWithToolset: false }), + }); + expect((await manager.enableToolset("core")).success).toBe(true); + const client = await connectClient(server); + + expect((await client.callTool({ name: "text", arguments: { value: "x" } })).isError).toBeFalsy(); + expect((await client.callTool({ name: "text", arguments: { value: 1 } })).isError).toBe(true); + expect((await client.callTool({ name: "count", arguments: { value: 1 } })).isError).toBeFalsy(); + expect((await client.callTool({ name: "count", arguments: { value: "x" } })).isError).toBe(true); + // The root $id is neither used nor advertised. + const { tools } = await client.listTools(); + expect(tools.every((t) => !("$id" in t.inputSchema))).toBe(true); + }); + + it("rolls back the tools registered before a bad JSON Schema tool", async () => { + const server = new McpServer({ name: "t", version: "1.0.0" }); + const resolver = new ModuleResolver({ + catalog: { + core: { + name: "Core", + description: "", + tools: [ + { name: "good", description: "Good", inputSchema: symbolSchema, handler: async () => ({ content: [] }) }, + { + name: "bad", + description: "Bad", + inputSchema: { type: "object", properties: { a: { type: "strng" } } }, + handler: async () => ({ content: [] }), + }, + ], + }, + } as any, + }); + const manager = new DynamicToolManager({ + server, + resolver, + toolRegistry: new ToolRegistry({ namespaceWithToolset: false }), + }); + const result = await manager.enableToolset("core"); + expect(result.success).toBe(false); + expect(result.code).toBe("E_VALIDATION"); + expect(result.message).toMatch(/Tool 'bad'/); + expect(manager.isActive("core")).toBe(false); + const client = await connectClient(server); + expect((await client.listTools()).tools).toEqual([]); + }); + + it("resolves absolute $refs to a nested $id while the root $id is dropped", async () => { + const { server, manager } = managerFor({ + $id: "https://example.com/root", + type: "object", + properties: { symbol: { $ref: "https://example.com/symbol" } }, + required: ["symbol"], + $defs: { symbol: { $id: "https://example.com/symbol", type: "string" } }, + }); + expect((await manager.enableToolset("core")).success).toBe(true); + const client = await connectClient(server); + expect((await client.callTool({ name: "price", arguments: { symbol: "AAPL" } })).isError).toBeFalsy(); + expect((await client.callTool({ name: "price", arguments: { symbol: 1 } })).isError).toBe(true); + }); + + it("re-enables a JSON Schema toolset with a $id after disabling it", async () => { + const { server, manager } = managerFor({ ...symbolSchema, $id: "https://example.com/symbol" }); + expect((await manager.enableToolset("core")).success).toBe(true); + expect((await manager.disableToolset("core")).success).toBe(true); + expect((await manager.enableToolset("core")).success).toBe(true); + const client = await connectClient(server); + const res = await client.callTool({ name: "price", arguments: { symbol: "AAPL" } }); + expect(res.isError).toBeFalsy(); + }); +}); + describe("DynamicToolManager disable removes tools", () => { const ping = { name: "ping", diff --git a/tests/e2e/jsonSchemaTools.e2e.test.ts b/tests/e2e/jsonSchemaTools.e2e.test.ts index 486ccc0..f73cceb 100644 --- a/tests/e2e/jsonSchemaTools.e2e.test.ts +++ b/tests/e2e/jsonSchemaTools.e2e.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { createMcpServer } from "../../src/index.js"; import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; import { McpServer } from "@modelcontextprotocol/server"; -import { getAvailablePort, extractTextContent } from "./helpers.js"; +import { getAvailablePort, extractTextContent, extractToolNames, parseToolResponse } from "./helpers.js"; describe("JSON Schema tool definitions E2E", () => { let server: Awaited>; @@ -55,6 +55,11 @@ describe("JSON Schema tool definitions E2E", () => { const price = tools.tools.find((t) => t.name === "quotes.price"); expect(price).toBeDefined(); expect(price?.annotations).toEqual({ readOnlyHint: true }); + expect(price?.inputSchema).toMatchObject({ + type: "object", + properties: { symbol: { type: "string" } }, + required: ["symbol"], + }); const result = await client.callTool({ name: "quotes.price", @@ -64,4 +69,108 @@ describe("JSON Schema tool definitions E2E", () => { await client.close(); }); + + it("rejects arguments that break the schema, for that call only", async () => { + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + { requestInit: { headers: { "mcp-client-id": "json-schema-invalid-client" } } } + ); + const client = new Client({ name: "test", version: "1.0.0" }); + await client.connect(transport); + + const bad = await client.callTool({ name: "quotes.price", arguments: { symbol: 42 } }); + expect(bad.isError).toBe(true); + expect(extractTextContent(bad)).toMatch(/Input validation error/); + + const good = await client.callTool({ name: "quotes.price", arguments: { symbol: "MSFT" } }); + expect(extractTextContent(good)).toBe("MSFT: 123.45"); + + await client.close(); + }); }); + +describe("JSON Schema tools with concurrent clients E2E", () => { + let server: Awaited>; + let port: number; + /** Same root and nested `$id`, different types: each client is validated by its own toolset. */ + const schemaFor = (type: "string" | "number") => ({ + $id: "https://example.com/args", + type: "object", + properties: { value: { $ref: "#/$defs/value" } }, + required: ["value"], + $defs: { value: { $id: "https://example.com/value", type } }, + }); + const echo = async ({ value }: { value: unknown }) => ({ + content: [{ type: "text" as const, text: String(value) }], + }); + + beforeAll(async () => { + port = await getAvailablePort(); + server = await createMcpServer({ + createServer: () => new McpServer({ name: "test-json-schema-multi", version: "1.0.0" }), + catalog: { + texts: { + name: "Texts", + description: "String tools", + tools: [{ name: "echo", description: "Echo", inputSchema: schemaFor("string"), handler: echo }], + }, + numbers: { + name: "Numbers", + description: "Number tools", + tools: [{ name: "echo", description: "Echo", inputSchema: schemaFor("number"), handler: echo }], + }, + }, + startup: { mode: "DYNAMIC" }, + http: { port }, + }); + await server.start(); + }); + + afterAll(async () => { + await server.close(); + }); + + async function connect(clientId: string) { + const client = new Client({ name: "test", version: "1.0.0" }); + await client.connect( + new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`), { + requestInit: { headers: { "mcp-client-id": clientId } }, + }) + ); + return client; + } + + const enable = async (client: Client, name: string) => + parseToolResponse<{ success: boolean }>( + await client.callTool({ name: "enable_toolset", arguments: { name } } as any) + ); + + it("keeps each client's toolsets and validation apart", async () => { + const a = await connect("json-schema-a"); + const b = await connect("json-schema-b"); + expect((await enable(a, "texts")).success).toBe(true); + expect((await enable(b, "numbers")).success).toBe(true); + + expect(extractToolNames(await a.listTools())).toContain("texts.echo"); + expect(extractToolNames(await a.listTools())).not.toContain("numbers.echo"); + expect(extractToolNames(await b.listTools())).toContain("numbers.echo"); + expect(extractToolNames(await b.listTools())).not.toContain("texts.echo"); + + expect(extractTextContent(await a.callTool({ name: "texts.echo", arguments: { value: "x" } }))).toBe("x"); + expect((await a.callTool({ name: "texts.echo", arguments: { value: 1 } })).isError).toBe(true); + expect(extractTextContent(await b.callTool({ name: "numbers.echo", arguments: { value: 1 } }))).toBe("1"); + expect((await b.callTool({ name: "numbers.echo", arguments: { value: "x" } })).isError).toBe(true); + + // A disables its toolset; B keeps validating and serving its own. + const disabled = parseToolResponse<{ success: boolean }>( + await a.callTool({ name: "disable_toolset", arguments: { name: "texts" } } as any) + ); + expect(disabled.success).toBe(true); + expect(extractToolNames(await a.listTools())).not.toContain("texts.echo"); + expect(extractTextContent(await b.callTool({ name: "numbers.echo", arguments: { value: 2 } }))).toBe("2"); + + await a.close(); + await b.close(); + }); +}); + diff --git a/tests/schemaValidatorScope.test.ts b/tests/schemaValidatorScope.test.ts new file mode 100644 index 0000000..e637be0 --- /dev/null +++ b/tests/schemaValidatorScope.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, vi } from "vitest"; + +const constructed = vi.hoisted(() => ({ count: 0 })); + +vi.mock("@modelcontextprotocol/server/validators/ajv", async (importOriginal) => { + const original = + await importOriginal(); + return { + ...original, + AjvJsonSchemaValidator: class extends original.AjvJsonSchemaValidator { + constructor() { + super(); + constructed.count += 1; + } + }, + }; +}); + +import { McpServer } from "@modelcontextprotocol/server"; +import { DynamicToolManager } from "../src/core/DynamicToolManager.js"; +import { ModuleResolver } from "../src/mode/ModuleResolver.js"; + +/** + * JSON Schema validators hold every compiled schema for their lifetime, so each + * client's manager must own one instead of sharing a process-wide validator. + */ +describe("JSON Schema validator scope", () => { + function newManager() { + return new DynamicToolManager({ + server: new McpServer({ name: "t", version: "1.0.0" }), + resolver: new ModuleResolver({ catalog: {} }), + }); + } + + it("creates one validator per manager, not one per process or per tool", () => { + const before = constructed.count; + newManager(); + newManager(); + expect(constructed.count - before).toBe(2); + }); +});