From 6a84927af81c2021c6902189759d72349e13559b Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 25 Aug 2026 21:05:44 +0800 Subject: [PATCH 1/6] chore: record delivery binding for upgrade-mcp-client --- .specgit.yaml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.specgit.yaml b/.specgit.yaml index a9a1a2730..eded93872 100644 --- a/.specgit.yaml +++ b/.specgit.yaml @@ -1,12 +1,8 @@ version: 1 -delivery: open-issues-batch +delivery: upgrade-mcp-client context: kind: branch - branch: feat/434-open-issues-batch + branch: refactor/447-upgrade-mcp-client issues: - - 434 - - 442 - - 443 - - 440 - - 436 -pr: 444 + - 447 + - 448 From 1ec20bca79e78e53a54632930af7321a3d93e1aa Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 25 Aug 2026 21:06:09 +0800 Subject: [PATCH 2/6] chore: record delivery binding for upgrade-mcp-client --- .specgit.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.specgit.yaml b/.specgit.yaml index eded93872..1a76cc0ea 100644 --- a/.specgit.yaml +++ b/.specgit.yaml @@ -6,3 +6,4 @@ context: issues: - 447 - 448 +pr: 449 From aa3fda5e09c6e8993110473b0f9a248e99657497 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 25 Aug 2026 22:03:07 +0800 Subject: [PATCH 3/6] refactor(mcp): migrate to SDK v2 and add per-server protocol era negotiation - swap @modelcontextprotocol/sdk@1.29.0 for @modelcontextprotocol/client@2.0.0 (+core, server dev-only); workspace zod ^4.2.0; drop stale v1 patch - remove StreamableHTTP->SSE fallback; Streamable HTTP failure is terminal - handler registration moved to method strings; oauth-provider types from client root; test mocks re-pointed to the v2 package root - ConfigMCPV1 local/remote gain optional protocol: auto|legacy|modern, mapped to ClientOptions.versionNegotiation (default auto probes server/discover with 2025 fallback; modern pins 2026-07-28) - regenerate JS SDK types; mcp auth diagnostics mirror the era mapping - behavior note: v2 removes transparent 404 session recovery and SSE-only (pre-2025-03-26) servers no longer connect; protocol: legacy is the escape hatch Closes #447 Closes #448 --- bun.lock | 137 +----- package.json | 3 +- packages/core/src/v1/config/mcp.ts | 8 + packages/opencode/package.json | 4 +- packages/opencode/src/cli/cmd/mcp.ts | 29 +- packages/opencode/src/mcp/catalog.ts | 11 +- packages/opencode/src/mcp/elicitation.ts | 77 ++-- packages/opencode/src/mcp/index.ts | 151 +++---- packages/opencode/src/mcp/oauth-provider.ts | 4 +- .../test/fixture/mcp-session-recovery.ts | 22 +- .../test/mcp/elicitation-transport.test.ts | 42 +- packages/opencode/test/mcp/headers.test.ts | 36 +- packages/opencode/test/mcp/lifecycle.test.ts | 52 +-- .../test/mcp/oauth-auto-connect.test.ts | 36 +- .../opencode/test/mcp/oauth-browser.test.ts | 33 +- packages/opencode/test/mcp/protocol.test.ts | 201 +++++++++ .../test/mcp/session-recovery.test.ts | 21 +- packages/sdk/js/src/v2/gen/types.gen.ts | 2 + .../@modelcontextprotocol%2Fsdk@1.29.0.patch | 427 ------------------ 19 files changed, 467 insertions(+), 829 deletions(-) create mode 100644 packages/opencode/test/mcp/protocol.test.ts delete mode 100644 patches/@modelcontextprotocol%2Fsdk@1.29.0.patch diff --git a/bun.lock b/bun.lock index ffc9b48d0..7f4ae59fd 100644 --- a/bun.lock +++ b/bun.lock @@ -570,7 +570,8 @@ "@effect/platform-node": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@gitlab/opencode-gitlab-auth": "1.3.3", - "@modelcontextprotocol/sdk": "1.29.0", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/core": "2.0.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", @@ -645,6 +646,7 @@ }, "devDependencies": { "@babel/core": "7.29.6", + "@modelcontextprotocol/server": "2.0.0", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/core": "workspace:*", "@opencode-ai/http-recorder": "workspace:*", @@ -1045,18 +1047,17 @@ "tree-sitter-bash", ], "patchedDependencies": { - "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", - "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", - "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", - "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", - "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", + "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", + "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch", "@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", + "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", }, "overrides": { "@opentui/core": "catalog:", @@ -1125,7 +1126,7 @@ "ulid": "3.0.1", "vite": "7.1.4", "vite-plugin-solid": "2.11.10", - "zod": "4.1.8", + "zod": "^4.2.0", }, "packages": { "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], @@ -1442,8 +1443,6 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@bruits/satteri-darwin-arm64": ["@bruits/satteri-darwin-arm64@0.9.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iw4nZgx9v30lWo/MTngQqi1pI78KI0DnkSm+lVJGYdmPLgAyDNJigVhpG42/Iq55A6c1Ll8q66ljyyRiQUxwow=="], "@bruits/satteri-darwin-x64": ["@bruits/satteri-darwin-x64@0.9.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-6T26Z5Kf3cFW2PSlk9p7zT7yVxvuBSiJvYyz9u8KjYwMTqZyIDOj2wDyNpxKV4+6yUVG7rddq2QwvG/8LJA2+Q=="], @@ -1676,8 +1675,6 @@ "@hey-api/types": ["@hey-api/types@0.1.4", "", {}, "sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg=="], - "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], - "@hono/standard-validator": ["@hono/standard-validator@0.2.0", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ=="], "@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="], @@ -1840,7 +1837,11 @@ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "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", "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" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw=="], + + "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA=="], + + "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "zod": "^4.2.0" } }, "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw=="], "@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="], @@ -2980,8 +2981,6 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.8", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.8", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.8", "vitest": "4.1.8" }, "optionalPeers": ["@vitest/browser"] }, "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@vitest/mocker": ["@vitest/mocker@4.1.7", "", { "dependencies": { "@vitest/spy": "4.1.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA=="], @@ -3104,8 +3103,6 @@ "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], - "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg=="], - "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], @@ -3366,7 +3363,7 @@ "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -3378,8 +3375,6 @@ "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], - "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], @@ -3590,8 +3585,6 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "engine.io-client": ["engine.io-client@6.6.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.20.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg=="], @@ -3688,8 +3681,6 @@ "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], - "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], - "expressive-code": ["expressive-code@0.44.1", "", { "dependencies": { "@expressive-code/core": "^0.44.1", "@expressive-code/plugin-frames": "^0.44.1", "@expressive-code/plugin-shiki": "^0.44.1", "@expressive-code/plugin-text-markers": "^0.44.1" } }, "sha512-GakidxhapWDzpKLqEaFQ8wGk6gAqEtPQibu8+yPBfnDLgev5Vdsh1pasTxnrXL/mzIknyqeTwhMHTghdaiUrTg=="], "exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="], @@ -4086,8 +4077,6 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], @@ -4126,12 +4115,6 @@ "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], - "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], - - "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], - - "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], - "iterate-iterator": ["iterate-iterator@1.0.2", "", {}, "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw=="], "iterate-value": ["iterate-value@1.0.2", "", { "dependencies": { "es-get-iterator": "^1.0.2", "iterate-iterator": "^1.0.1" } }, "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ=="], @@ -4302,8 +4285,6 @@ "magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], - "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], - "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], @@ -4998,8 +4979,6 @@ "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -5610,7 +5589,7 @@ "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], - "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-openapi": ["zod-openapi@5.4.6", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-P2jsOOBAq/6hCwUsMCjUATZ8szkMsV5VAwZENfyxp2Hc/XPJQpVwAgevWZc65xZauCwWB9LAn7zYeiCJFAEL+A=="], @@ -5734,8 +5713,6 @@ "@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@astrojs/sitemap/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@astrojs/solid-js/vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], "@astrojs/starlight/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -5960,15 +5937,7 @@ "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "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" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], - - "@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - - "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@modelcontextprotocol/client/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], "@npmcli/arborist/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -6128,10 +6097,6 @@ "@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], - "@standard-community/standard-json/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - - "@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - "@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -6172,8 +6137,6 @@ "@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], - "@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], - "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], @@ -6220,10 +6183,6 @@ "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "ast-v8-to-istanbul/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], - "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="], "astro/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.2.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^3.0.0", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg=="], @@ -6250,8 +6209,6 @@ "astro/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], - "astro/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "babel-dead-code-elimination/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], @@ -6260,6 +6217,8 @@ "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "body-parser/content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -6330,8 +6289,6 @@ "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], - "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "engine.io-client/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], "env-runner/crossws": ["crossws@0.4.12", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-aypfsr6t0uNvkqaZc6zvBfXzC6pLI0/sIulpkV6RwCVtZqG5ebBzv4weImKK0VNCj91Wl9F5j7p5WU4MNrybng=="], @@ -6350,6 +6307,8 @@ "execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + "express/content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -6390,8 +6349,6 @@ "ignore-walk/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "istanbul-reports/html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], - "js-beautify/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "js-beautify/nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="], @@ -6494,8 +6451,6 @@ "roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], - "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], @@ -6870,58 +6825,28 @@ "@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - - "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], - - "@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "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" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "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" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], - "@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], - "@octokit/auth-app/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/auth-app/@octokit/request-error/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], "@octokit/auth-oauth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-app/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], - "@octokit/auth-oauth-app/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/auth-oauth-app/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-oauth-device/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-device/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], - "@octokit/auth-oauth-device/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/auth-oauth-device/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-oauth-user/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-user/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], - "@octokit/auth-oauth-user/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/auth-oauth-user/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -6934,14 +6859,10 @@ "@octokit/graphql/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], - "@octokit/graphql/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], "@octokit/oauth-methods/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], - "@octokit/oauth-methods/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/plugin-paginate-rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], @@ -7036,10 +6957,6 @@ "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], - "@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], @@ -7056,8 +6973,6 @@ "@vitejs/plugin-react/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="], - "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -7804,10 +7719,6 @@ "@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - - "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-app/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -7816,20 +7727,14 @@ "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], - "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/plugin-paginate-rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], - "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], - "@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], diff --git a/package.json b/package.json index c4a0712a8..8a0fdf97d 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "semver": "7.7.4", "typescript": "5.8.2", "@typescript/native-preview": "7.0.0-dev.20251207.1", - "zod": "4.1.8", + "zod": "^4.2.0", "remeda": "2.26.0", "sst": "4.13.1", "shiki": "4.2.0", @@ -153,7 +153,6 @@ "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", - "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch" } } diff --git a/packages/core/src/v1/config/mcp.ts b/packages/core/src/v1/config/mcp.ts index 0a2aeff12..7070681c3 100644 --- a/packages/core/src/v1/config/mcp.ts +++ b/packages/core/src/v1/config/mcp.ts @@ -20,6 +20,10 @@ export const Local = Schema.Struct({ timeout: Schema.optional(PositiveInt).annotate({ description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", }), + protocol: Schema.optional(Schema.Literals(["auto", "legacy", "modern"])).annotate({ + description: + "Protocol era negotiation: 'auto' probes server/discover and falls back to the 2025 initialize handshake, 'legacy' skips the probe and runs the 2025 initialize handshake, 'modern' speaks only 2026-07-28 with no fallback. Defaults to 'auto'.", + }), }).annotate({ identifier: "McpLocalConfig" }) export type Local = Schema.Schema.Type @@ -56,6 +60,10 @@ export const Remote = Schema.Struct({ timeout: Schema.optional(PositiveInt).annotate({ description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", }), + protocol: Schema.optional(Schema.Literals(["auto", "legacy", "modern"])).annotate({ + description: + "Protocol era negotiation: 'auto' probes server/discover and falls back to the 2025 initialize handshake, 'legacy' skips the probe and runs the 2025 initialize handshake, 'modern' speaks only 2026-07-28 with no fallback. Defaults to 'auto'.", + }), }).annotate({ identifier: "McpRemoteConfig" }) export type Remote = Schema.Schema.Type diff --git a/packages/opencode/package.json b/packages/opencode/package.json index b34b67726..dd4cb97cd 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -40,6 +40,7 @@ "@tsconfig/bun": "catalog:", "@types/babel__core": "7.20.5", "@types/bun": "catalog:", + "@modelcontextprotocol/server": "2.0.0", "@types/cross-spawn": "catalog:", "@types/mime-types": "3.0.1", "@types/escape-html": "1.0.3", @@ -84,7 +85,8 @@ "@effect/platform-node": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@gitlab/opencode-gitlab-auth": "1.3.3", - "@modelcontextprotocol/sdk": "1.29.0", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/core": "2.0.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index b42651dc1..26090e105 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -2,10 +2,12 @@ import { cmd } from "./cmd" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { effectCmd } from "../effect-cmd" import { Cause } from "effect" -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" -import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" -import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js" +import { + Client, + StreamableHTTPClientTransport, + UnauthorizedError, + LATEST_PROTOCOL_VERSION, +} from "@modelcontextprotocol/client" import * as prompts from "@clack/prompts" import { UI } from "../ui" import { MCP } from "../../mcp" @@ -787,10 +789,21 @@ export const McpDebugCommand = effectCmd({ }) try { - const client = new Client({ - name: "opencode-debug", - version: InstallationVersion, - }) + const client = new Client( + { name: "opencode-debug", version: InstallationVersion }, + { + // Mirror createClient's per-server era mapping so diagnostics + // match the runtime connection behavior (#448). + versionNegotiation: { + mode: + serverConfig.protocol === "legacy" + ? "legacy" + : serverConfig.protocol === "modern" + ? { pin: "2026-07-28" } + : "auto", + }, + }, + ) await client.connect(transport) prompts.log.success("Connection successful (already authenticated)") await client.close() diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts index 15eb5af40..0dea78a78 100644 --- a/packages/opencode/src/mcp/catalog.ts +++ b/packages/opencode/src/mcp/catalog.ts @@ -1,10 +1,6 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { - CallToolResultSchema, - ListToolsResultSchema, - ToolSchema, - type Tool as MCPToolDef, -} from "@modelcontextprotocol/sdk/types.js" +import { ListToolsResultSchema, ToolSchema } from "@modelcontextprotocol/core" +import { Client } from "@modelcontextprotocol/client" +import type { Tool as MCPToolDef } from "@modelcontextprotocol/client" import { dynamicTool, jsonSchema, type JSONSchema7, type Tool } from "ai" import { Effect } from "effect" @@ -56,7 +52,6 @@ export function convertTool(mcpTool: MCPToolDef, client: Client, timeout?: numbe name: mcpTool.name, arguments: (args || {}) as Record, }, - CallToolResultSchema, { resetTimeoutOnProgress: true, signal: options.abortSignal, diff --git a/packages/opencode/src/mcp/elicitation.ts b/packages/opencode/src/mcp/elicitation.ts index 6b584d0b1..98382cca1 100644 --- a/packages/opencode/src/mcp/elicitation.ts +++ b/packages/opencode/src/mcp/elicitation.ts @@ -19,8 +19,7 @@ * ─▶ respond to server */ import { Effect, Option, Schema } from "effect" -import type { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js" +import type { Client } from "@modelcontextprotocol/client" import { Question } from "@/question" import { SettingsHook, type TriggerResult } from "@/hook/settings" import { Notification } from "@/notification" @@ -85,10 +84,7 @@ interface FieldSpec { * reason. MCP form-mode allows flat objects whose properties are primitive * string/number/integer/boolean, optionally with an enum. */ -export function classifyProperty( - name: string, - prop: unknown, -): { field: FieldSpec } | { reject: string } { +export function classifyProperty(name: string, prop: unknown): { field: FieldSpec } | { reject: string } { if (typeof prop !== "object" || prop === null || Array.isArray(prop)) return { reject: `property "${name}" must be an object` } const p = prop as Record @@ -97,19 +93,34 @@ export function classifyProperty( if (Array.isArray(p.enum)) { const enumValues = p.enum.filter((v): v is string => typeof v === "string") if (enumValues.length !== p.enum.length) return { reject: `property "${name}" enum must be all strings` } - return { field: { name, description: typeof p.description === "string" ? p.description : undefined, kind: "enum", enumValues } } + return { + field: { + name, + description: typeof p.description === "string" ? p.description : undefined, + kind: "enum", + enumValues, + }, + } } if (type === "boolean") { - return { field: { name, description: typeof p.description === "string" ? p.description : undefined, kind: "boolean" } } + return { + field: { name, description: typeof p.description === "string" ? p.description : undefined, kind: "boolean" }, + } } if (type === "string") { - return { field: { name, description: typeof p.description === "string" ? p.description : undefined, kind: "string" } } + return { + field: { name, description: typeof p.description === "string" ? p.description : undefined, kind: "string" }, + } } if (type === "number") { - return { field: { name, description: typeof p.description === "string" ? p.description : undefined, kind: "number" } } + return { + field: { name, description: typeof p.description === "string" ? p.description : undefined, kind: "number" }, + } } if (type === "integer") { - return { field: { name, description: typeof p.description === "string" ? p.description : undefined, kind: "integer" } } + return { + field: { name, description: typeof p.description === "string" ? p.description : undefined, kind: "integer" }, + } } return { reject: `property "${name}" type "${String(type)}" not supported (flat primitives only)` } } @@ -276,13 +287,11 @@ export const handleElicitation = Effect.fn("MCP.elicitation.handle")(function* ( // decline without surfacing the Question. if (settingsHook) { const hookResult = yield* settingsHook - .trigger( - { event: "Elicitation", prompt: input.message, schema: input.requestedSchema } as never, - { sessionID, transcriptPath: "" }, - ) - .pipe( - Effect.catch(() => Effect.succeed({ additionalContexts: [], systemMessages: [] })), - ) + .trigger({ event: "Elicitation", prompt: input.message, schema: input.requestedSchema } as never, { + sessionID, + transcriptPath: "", + }) + .pipe(Effect.catch(() => Effect.succeed({ additionalContexts: [], systemMessages: [] }))) yield* SettingsHook.landSystemMessages(hookResult as TriggerResult, { sessionID }) if ((hookResult as TriggerResult).blocked) { yield* Effect.logWarning("elicitation declined: hook blocked") @@ -301,14 +310,12 @@ export const handleElicitation = Effect.fn("MCP.elicitation.handle")(function* ( // rejection (user dismissed) both resolve to decline; only a validated reply // resolves to accept. const fields = fieldSpecsFromSchema(input.requestedSchema) - const validated = yield* question - .ask({ sessionID: SessionID.make(sessionID), questions: mapped.questions }) - .pipe( - // timeoutOption returns None on timeout; timeoutOrElse would also work. - Effect.timeoutOption(ELICITATION_TIMEOUT_MS), - Effect.map((opt) => (Option.isNone(opt) ? undefined : validateAndCoerce(fields, opt.value))), - Effect.catch(() => Effect.succeed(undefined)), // user reject (RejectedError) → decline - ) + const validated = yield* question.ask({ sessionID: SessionID.make(sessionID), questions: mapped.questions }).pipe( + // timeoutOption returns None on timeout; timeoutOrElse would also work. + Effect.timeoutOption(ELICITATION_TIMEOUT_MS), + Effect.map((opt) => (Option.isNone(opt) ? undefined : validateAndCoerce(fields, opt.value))), + Effect.catch(() => Effect.succeed(undefined)), // user reject (RejectedError) → decline + ) // ElicitationResult hook fires on resolution (result on accept, cancelled otherwise). if (settingsHook) { @@ -330,17 +337,11 @@ export const handleElicitation = Effect.fn("MCP.elicitation.handle")(function* ( * Register the elicitation handler on a connected MCP client. The handler is a * plain async function (Promise-returning) that bridges into the Effect world. */ -export function registerElicitationHandler( - client: Client, - bridge: import("@/effect/bridge").EffectBridge.Shape, -) { - // Dynamic import keeps the protocol schema lazy — the MCP SDK is only pulled - // in when elicitation is actually wired, not at module-eval time of callers. +export function registerElicitationHandler(client: Client, bridge: import("@/effect/bridge").EffectBridge.Shape) { // The handler receives the full JSON-RPC request `{ method, params: {...} }`; - // the elicitation fields live under `params`. - const handler = async (request: { - params?: { message?: string; requestedSchema?: unknown; mode?: string } - }) => { + // the elicitation fields live under `params`. On 2026-era connections the + // client drives input_required retries through this same handler. + const handler = async (request: { params?: { message?: string; requestedSchema?: unknown; mode?: string } }) => { const params = request.params ?? {} const sessionID = SessionContext.sessionID ?? activeSession.at(-1)?.id const response = await bridge.promise( @@ -353,7 +354,5 @@ export function registerElicitationHandler( ) return response } - // The schema is imported eagerly at module load so registration is synchronous - // (a lazy dynamic import would race with the first incoming request). - client.setRequestHandler(ElicitRequestSchema, handler as never) + client.setRequestHandler("elicitation/create", handler as never) } diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 83f9e4122..91d253a83 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -4,18 +4,9 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { type Tool } from "ai" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { serviceUse } from "@opencode-ai/core/effect/service-use" -import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js" -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" -import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" -import { - ListRootsRequestSchema, - type LoggingMessageNotification, - LoggingMessageNotificationSchema, - type Tool as MCPToolDef, - ToolListChangedNotificationSchema, -} from "@modelcontextprotocol/sdk/types.js" +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio" +import { Client, StreamableHTTPClientTransport, UnauthorizedError } from "@modelcontextprotocol/client" +import type { ClientOptions, LoggingMessageNotification, Tool as MCPToolDef } from "@modelcontextprotocol/client" import { Config } from "@/config/config" import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp" import { NamedError } from "@opencode-ai/core/util/error" @@ -75,11 +66,17 @@ export class NotFoundError extends Schema.TaggedErrorClass()("MCP type MCPClient = Client -function createClient(directory: string) { - const client = new Client({ name: "opencode", version: InstallationVersion }, CLIENT_OPTIONS) - client.setRequestHandler(ListRootsRequestSchema, () => - Promise.resolve({ roots: [{ uri: pathToFileURL(directory).href }] }), - ) +function createClient(directory: string, protocol?: "auto" | "legacy" | "modern") { + const client = new Client({ name: "opencode", version: InstallationVersion }, { + ...CLIENT_OPTIONS, + // Per-server protocol era (#448). Absent/'auto' probes server/discover with + // conservative fallback to the 2025 initialize handshake; 'legacy' skips the + // probe; 'modern' pins 2026-07-28 with no fallback. + versionNegotiation: { + mode: protocol === "legacy" ? "legacy" : protocol === "modern" ? { pin: "2026-07-28" } : "auto", + }, + }) + client.setRequestHandler("roots/list", () => Promise.resolve({ roots: [{ uri: pathToFileURL(directory).href }] })) return client } @@ -110,8 +107,7 @@ export const Status = Schema.Union([ export type Status = Schema.Schema.Type // Store transports for OAuth servers to allow finishing auth -type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport -const pendingOAuthTransports = new Map() +const pendingOAuthTransports = new Map() // Prompt cache types type PromptInfo = Awaited>["prompts"][number] @@ -203,20 +199,24 @@ export const layer = Layer.effect( const auth = yield* McpAuth.Service const events = yield* EventV2Bridge.Service - type Transport = StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport + type Transport = StdioClientTransport | StreamableHTTPClientTransport /** * Connect a client via the given transport with resource safety: * on failure the transport is closed; on success the caller owns it. */ - const connectTransport = Effect.fn("MCP.connectTransport")(function* (transport: Transport, timeout: number) { + const connectTransport = Effect.fn("MCP.connectTransport")(function* ( + transport: Transport, + timeout: number, + protocol?: "auto" | "legacy" | "modern", + ) { const directory = yield* InstanceState.directory return yield* Effect.acquireUseRelease( Effect.succeed(transport), (t) => Effect.tryPromise({ try: () => { - const client = createClient(directory) + const client = createClient(directory, protocol) return withTimeout(client.connect(t), timeout).then(() => client) }, catch: (e) => (e instanceof Error ? e : new Error(String(e))), @@ -260,70 +260,55 @@ export const layer = Layer.effect( ) } - const transports: Array<{ name: string; transport: TransportWithAuth }> = [ - { - name: "StreamableHTTP", - transport: new StreamableHTTPClientTransport(url, { - authProvider, - requestInit: mcp.headers ? { headers: mcp.headers } : undefined, - }), - }, - { - name: "SSE", - transport: new SSEClientTransport(url, { - authProvider, - requestInit: mcp.headers ? { headers: mcp.headers } : undefined, - }), - }, - ] + // The dual StreamableHTTP→SSE fallback was removed with the v2 migration: + // Streamable HTTP failure is terminal (SSE-only servers predate 2025-03-26). + const transport = new StreamableHTTPClientTransport(url, { + authProvider, + requestInit: mcp.headers ? { headers: mcp.headers } : undefined, + }) const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT let lastStatus: Status | undefined - for (const { name, transport } of transports) { - const result = yield* connectTransport(transport, connectTimeout).pipe( - Effect.map((client) => ({ client, transportName: name })), - Effect.catch((error) => { - const lastError = error instanceof Error ? error : new Error(String(error)) - const isAuthError = - error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth")) - - if (isAuthError) { - if (lastError.message.includes("registration") || lastError.message.includes("client_id")) { - lastStatus = { - status: "needs_client_registration" as const, - error: "Server does not support dynamic client registration. Please provide clientId in config.", - } - return events - .publish(TuiEvent.ToastShow, { - title: "MCP Authentication Required", - message: `Server "${key}" requires a pre-registered client ID. Add clientId to your config.`, - variant: "warning", - duration: 8000, - }) - .pipe(Effect.ignore, Effect.as(undefined)) - } else { - pendingOAuthTransports.set(key, transport) - lastStatus = { status: "needs_auth" as const } - return events - .publish(TuiEvent.ToastShow, { - title: "MCP Authentication Required", - message: `Server "${key}" requires authentication. Run: opencode mcp auth ${key}`, - variant: "warning", - duration: 8000, - }) - .pipe(Effect.ignore, Effect.as(undefined)) + const result = yield* connectTransport(transport, connectTimeout, mcp.protocol).pipe( + Effect.catch((error) => { + const lastError = error instanceof Error ? error : new Error(String(error)) + const isAuthError = + error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth")) + + if (isAuthError) { + if (lastError.message.includes("registration") || lastError.message.includes("client_id")) { + lastStatus = { + status: "needs_client_registration" as const, + error: "Server does not support dynamic client registration. Please provide clientId in config.", } + return events + .publish(TuiEvent.ToastShow, { + title: "MCP Authentication Required", + message: `Server "${key}" requires a pre-registered client ID. Add clientId to your config.`, + variant: "warning", + duration: 8000, + }) + .pipe(Effect.ignore, Effect.as(undefined)) + } else { + pendingOAuthTransports.set(key, transport) + lastStatus = { status: "needs_auth" as const } + return events + .publish(TuiEvent.ToastShow, { + title: "MCP Authentication Required", + message: `Server "${key}" requires authentication. Run: opencode mcp auth ${key}`, + variant: "warning", + duration: 8000, + }) + .pipe(Effect.ignore, Effect.as(undefined)) } + } - lastStatus = { status: "failed" as const, error: lastError.message } - return Effect.void - }), - ) - if (result) return { client: result.client, status: { status: "connected" } as Status } - // If this was an auth error, stop trying other transports - if (lastStatus?.status === "needs_auth" || lastStatus?.status === "needs_client_registration") break - } + lastStatus = { status: "failed" as const, error: lastError.message } + return Effect.void + }), + ) + if (result) return { client: result, status: { status: "connected" } as Status } return { client: undefined as MCPClient | undefined, @@ -351,7 +336,7 @@ export const layer = Layer.effect( }) const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT - return yield* connectTransport(transport, connectTimeout).pipe( + return yield* connectTransport(transport, connectTimeout, mcp.protocol).pipe( Effect.map((client): { client: MCPClient | undefined; status: Status } => ({ client, status: { status: "connected" }, @@ -452,12 +437,12 @@ export const layer = Layer.effect( ) } - client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => + client.setNotificationHandler("notifications/message", (notification) => bridge.promise(serverLog(name, notification.params)), ) if (!client.getServerCapabilities()?.tools) return - client.setNotificationHandler(ToolListChangedNotificationSchema, async () => { + client.setNotificationHandler("notifications/tools/list_changed", async () => { if (s.clients[name] !== client || s.status[name]?.status !== "connected") return const listed = await bridge.promise(McpCatalog.defs(client, timeout)) @@ -850,7 +835,7 @@ export const layer = Layer.effect( return yield* Effect.tryPromise({ try: () => { - const client = createClient(directory) + const client = createClient(directory, mcpConfig.protocol) return client .connect(transport) .then(() => ({ authorizationUrl: "", oauthState, client }) satisfies AuthResult) diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts index aa29777f5..6c4235859 100644 --- a/packages/opencode/src/mcp/oauth-provider.ts +++ b/packages/opencode/src/mcp/oauth-provider.ts @@ -1,10 +1,10 @@ -import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js" import type { + OAuthClientProvider, OAuthClientMetadata, OAuthTokens, OAuthClientInformation, OAuthClientInformationFull, -} from "@modelcontextprotocol/sdk/shared/auth.js" +} from "@modelcontextprotocol/client" import { Effect } from "effect" import { McpAuth } from "./auth" diff --git a/packages/opencode/test/fixture/mcp-session-recovery.ts b/packages/opencode/test/fixture/mcp-session-recovery.ts index c20fb5aa5..17b7772a7 100644 --- a/packages/opencode/test/fixture/mcp-session-recovery.ts +++ b/packages/opencode/test/fixture/mcp-session-recovery.ts @@ -1,9 +1,6 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" -import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js" +import { Client, StreamableHTTPClientTransport, LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/client" const posts: Array<{ method: string; session: string | null }> = [] -let initializeCount = 0 let pingCount = 0 const server = Bun.serve({ port: 0, @@ -16,7 +13,6 @@ const server = Bun.serve({ posts.push({ method: message.method, session }) if (message.method === "initialize") { - initializeCount++ return Response.json( { jsonrpc: "2.0", @@ -27,7 +23,7 @@ const server = Bun.serve({ serverInfo: { name: "test", version: "1" }, }, }, - { headers: { "mcp-session-id": initializeCount === 1 ? "expired" : "replacement" } }, + { headers: { "mcp-session-id": "expired" } }, ) } @@ -40,11 +36,23 @@ const server = Bun.serve({ }) const client = new Client({ name: "test", version: "1" }) +let pingError: unknown try { await client.connect(new StreamableHTTPClientTransport(server.url)) await client.ping() - process.stdout.write(JSON.stringify(posts)) +} catch (error) { + pingError = error } finally { await client.close() server.stop(true) } + +const shape = + typeof pingError === "object" && + pingError !== null && + "name" in pingError && + "code" in pingError && + "status" in pingError + ? { name: String(pingError.name), code: String(pingError.code), status: Number(pingError.status) } + : null +process.stdout.write(JSON.stringify({ posts, error: shape })) diff --git a/packages/opencode/test/mcp/elicitation-transport.test.ts b/packages/opencode/test/mcp/elicitation-transport.test.ts index 716e77764..f5f5a7ec5 100644 --- a/packages/opencode/test/mcp/elicitation-transport.test.ts +++ b/packages/opencode/test/mcp/elicitation-transport.test.ts @@ -1,17 +1,15 @@ // Bypass Bun's process-global mock registry AND module cache. Sibling MCP -// tests register `mock.module("@modelcontextprotocol/sdk/client/index.js", ...)` +// tests register `mock.module("@modelcontextprotocol/client", ...)` // with reduced MockClient implementations (no transport, no `callTool`); // `mock.restore()` clears the registry, but Bun's module cache is keyed by // specifier and retains the previously-resolved *mock instance* for that path. // We therefore pull types from the public path via a type-only import, but at -// runtime load Client from the dist/esm path — a different specifier → a -// different cache entry → the real SDK Client with full callTool support. +// runtime load Client from a cache-busted specifier — a different specifier → +// a different cache entry → the real SDK Client with full callTool support. import { afterEach, beforeEach, describe, expect, mock } from "bun:test" import { Cause, Effect, Fiber, Layer, Exit } from "effect" -import { Server } from "@modelcontextprotocol/sdk/server/index.js" -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" -import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" -import type { Client as ClientType } from "@modelcontextprotocol/sdk/client/index.js" +import type { Client as ClientType } from "@modelcontextprotocol/client" +import { Server, InMemoryTransport } from "@modelcontextprotocol/server" mock.restore() // Cache-bust the import: add a unique query string to force Bun to load a fresh @@ -21,7 +19,7 @@ mock.restore() // any cached (mocked) module instances and get the real SDK Client with full // `callTool` support. const cacheBuster = `?bust=${Date.now()}` -const { Client } = (await import(`@modelcontextprotocol/sdk/client${cacheBuster}`)) as unknown as { +const { Client } = (await import(`@modelcontextprotocol/client${cacheBuster}`)) as unknown as { Client: typeof ClientType } import { Question } from "@/question" @@ -100,12 +98,10 @@ const STALE_SESSION = "ses_elicitation_transport_stale" */ function makeStubServer(): Server { const server = new Server({ name: "elicitation-stub", version: "1.0.0" }, { capabilities: { tools: {} } }) - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: [ - { name: "pick", description: "Elicit a color choice", inputSchema: { type: "object", properties: {} } }, - ], + server.setRequestHandler("tools/list", async () => ({ + tools: [{ name: "pick", description: "Elicit a color choice", inputSchema: { type: "object", properties: {} } }], })) - server.setRequestHandler(CallToolRequestSchema, async () => { + server.setRequestHandler("tools/call", async () => { const result = await server.elicitInput({ message: "Pick a color", requestedSchema: { @@ -170,7 +166,9 @@ describe("mcp elicitation — real transport round-trip (5.4)", () => { const staleCleanup = setActiveElicitationSession(STALE_SESSION) yield* Effect.addFinalizer(() => Effect.sync(staleCleanup)) staleCleanup() - const callFiber = yield* Effect.promise(() => client.callTool({ name: "pick", arguments: {} })).pipe(Effect.forkScoped) + const callFiber = yield* Effect.promise(() => client.callTool({ name: "pick", arguments: {} })).pipe( + Effect.forkScoped, + ) // The elicitation surfaces as a pending Question. Filter by this test's // SESSION so a pending question leaked from a prior file (bun runs the @@ -191,14 +189,13 @@ describe("mcp elicitation — real transport round-trip (5.4)", () => { // separates a silent decline (resolved with `elicit decline`) from a // genuine hang (still pending), and the list snapshot shows whether // the Question surfaced at all. - Effect.catch( - () => - Effect.fail( - new Error( - "elicitation never surfaced as a Question — " + - `question.list()=${JSON.stringify(lastItems)}; callFiber: ${describeFiber(callFiber)}`, - ), + Effect.catch(() => + Effect.fail( + new Error( + "elicitation never surfaced as a Question — " + + `question.list()=${JSON.stringify(lastItems)}; callFiber: ${describeFiber(callFiber)}`, ), + ), ), ) expect(String(pending[0].sessionID)).toBe(SESSION) @@ -217,7 +214,8 @@ describe("mcp elicitation — real transport round-trip (5.4)", () => { expect(events.filter((p) => p.event === "Elicitation")).toHaveLength(1) expect( events.filter( - (p) => p.event === "ElicitationResult" && JSON.stringify((p as { result?: unknown }).result).includes("green"), + (p) => + p.event === "ElicitationResult" && JSON.stringify((p as { result?: unknown }).result).includes("green"), ), ).toHaveLength(1) }), diff --git a/packages/opencode/test/mcp/headers.test.ts b/packages/opencode/test/mcp/headers.test.ts index c51ed00d3..5d97141af 100644 --- a/packages/opencode/test/mcp/headers.test.ts +++ b/packages/opencode/test/mcp/headers.test.ts @@ -4,13 +4,22 @@ import { testEffect } from "../lib/effect" // Track what options were passed to each transport constructor const transportCalls: Array<{ - type: "streamable" | "sse" + type: "streamable" url: string options: { authProvider?: unknown; requestInit?: RequestInit } }> = [] -// Mock the transport constructors to capture their arguments -void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ +class MockUnauthorizedError extends Error { + constructor() { + super("Unauthorized") + this.name = "UnauthorizedError" + } +} + +// v2 packs Client, StreamableHTTPClientTransport, and UnauthorizedError into +// the single package root, so one mock.module covers the whole surface. The +// Client mock just bridges connect() into the (throwing) mock transport. +void mock.module("@modelcontextprotocol/client", () => ({ StreamableHTTPClientTransport: class MockStreamableHTTP { constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) { transportCalls.push({ @@ -23,20 +32,13 @@ void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ throw new Error("Mock transport cannot connect") } }, -})) - -void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ - SSEClientTransport: class MockSSE { - constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) { - transportCalls.push({ - type: "sse", - url: url.toString(), - options: options ?? {}, - }) - } - async start() { - throw new Error("Mock transport cannot connect") + UnauthorizedError: MockUnauthorizedError, + Client: class MockClient { + setRequestHandler() {} + async connect(transport: { start: () => Promise }) { + await transport.start() } + async close() {} }, })) @@ -63,7 +65,7 @@ describe("mcp.headers", () => { }) .pipe(Effect.catch(() => Effect.void)) - // Both transports should have been created with headers + // The transport should have been created with headers expect(transportCalls.length).toBeGreaterThanOrEqual(1) for (const call of transportCalls) { diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index cb89b8f94..818e83853 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -1,7 +1,6 @@ import path from "node:path" import { pathToFileURL } from "node:url" import { expect, mock, beforeEach, afterAll } from "bun:test" -import { ListRootsRequestSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js" import { Cause, Effect, Exit } from "effect" import type { MCP as MCPNS } from "../../src/mcp/index" import { testEffect } from "../lib/effect" @@ -125,40 +124,25 @@ class MockStreamableHTTP { async finishAuth() {} } -class MockSSE { - // oxlint-disable-next-line no-useless-constructor - constructor(_url: URL, _opts?: any) {} - async start() { - if (connectShouldHang) return new Promise(() => {}) // never resolves - if (connectShouldFail) throw new Error(connectError) - } - async close() { - transportCloseCount++ +class MockUnauthorizedError extends Error { + constructor() { + super("Unauthorized") + this.name = "UnauthorizedError" } } -void mock.module("@modelcontextprotocol/sdk/client/stdio.js", () => ({ +void mock.module("@modelcontextprotocol/client/stdio", () => ({ StdioClientTransport: MockStdioTransport, })) -void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ - StreamableHTTPClientTransport: MockStreamableHTTP, -})) - -void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ - SSEClientTransport: MockSSE, -})) - -void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ - UnauthorizedError: class extends Error { - constructor() { - super("Unauthorized") - } - }, -})) - +// v2 packs Client, StreamableHTTPClientTransport, and UnauthorizedError into +// the single package root, so one mock.module covers the whole surface src +// imports. Every export src binds at runtime must be present — a missing +// UnauthorizedError would make `instanceof` throw instead of classify. // Mock Client that delegates to per-name MockClientState -void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ +void mock.module("@modelcontextprotocol/client", () => ({ + StreamableHTTPClientTransport: MockStreamableHTTP, + UnauthorizedError: MockUnauthorizedError, Client: class MockClient { _state!: MockClientState transport: any @@ -302,7 +286,7 @@ it.instance( expect(state.clientOptions?.capabilities?.roots).toEqual({}) expect(state.clientOptions?.capabilities?.roots?.listChanged).toBeUndefined() - const handler = state.requestHandlers.get(ListRootsRequestSchema) + const handler = state.requestHandlers.get("roots/list") expect(handler).toBeDefined() const result = yield* Effect.promise(() => handler?.() ?? Promise.reject(new Error("roots handler missing"))) expect(result).toEqual({ roots: [{ uri: pathToFileURL(directory).href }] }) @@ -540,7 +524,7 @@ it.instance( { name: "next_tool", description: "next", inputSchema: { type: "object", properties: {} } }, ] - const handler = serverState.notificationHandlers.get(ToolListChangedNotificationSchema) + const handler = serverState.notificationHandlers.get("notifications/tools/list_changed") expect(handler).toBeDefined() yield* Effect.promise(() => handler?.()) @@ -1280,11 +1264,11 @@ it.instance( ) // ======================================================================== -// Test: transport leak — failed remote transports not closed (#19168) +// Test: transport leak — failed remote transport not closed (#19168) // ======================================================================== it.instance( - "failed remote transport is closed before trying next transport", + "failed remote transport is closed on connect failure", () => MCP.Service.use((mcp: MCPNS.Interface) => Effect.gen(function* () { @@ -1302,8 +1286,8 @@ it.instance( const serverStatus = (addResult.status as any)["fail-remote"] ?? addResult.status expect(serverStatus.status).toBe("failed") - // Both StreamableHTTP and SSE transports should be closed - expect(transportCloseCount).toBeGreaterThanOrEqual(2) + // The StreamableHTTP transport should be closed (no SSE fallback in v2) + expect(transportCloseCount).toBeGreaterThanOrEqual(1) }), ), { config: { mcp: {} } }, diff --git a/packages/opencode/test/mcp/oauth-auto-connect.test.ts b/packages/opencode/test/mcp/oauth-auto-connect.test.ts index d94efbba0..87bebce2a 100644 --- a/packages/opencode/test/mcp/oauth-auto-connect.test.ts +++ b/packages/opencode/test/mcp/oauth-auto-connect.test.ts @@ -12,7 +12,7 @@ class MockUnauthorizedError extends Error { // Track what options were passed to each transport constructor const transportCalls: Array<{ - type: "streamable" | "sse" + type: "streamable" url: string options: { authProvider?: unknown } }> = [] @@ -24,8 +24,10 @@ let connectSucceedsImmediately = false let serverCapabilities: { tools?: object; resources?: object } = { tools: {} } let listToolsCalls = 0 -// Mock the transport constructors to simulate OAuth auto-auth on 401 -void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ +// v2 packs Client, StreamableHTTPClientTransport, and UnauthorizedError into +// the single package root, so one mock.module covers the whole surface. The +// transport simulates OAuth auto-auth on 401 like the real SDK does. +void mock.module("@modelcontextprotocol/client", () => ({ StreamableHTTPClientTransport: class MockStreamableHTTP { authProvider: | { @@ -67,25 +69,10 @@ void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ } async finishAuth(_code: string) {} }, -})) - -void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ - SSEClientTransport: class MockSSE { - constructor(url: URL, options?: { authProvider?: unknown }) { - transportCalls.push({ - type: "sse", - url: url.toString(), - options: options ?? {}, - }) - } - async start() { - throw new Error("Mock SSE transport cannot connect") - } - }, -})) - -// Mock the MCP SDK Client -void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ + // UnauthorizedError in the same module mock so src's instanceof check sees + // the same class the mock transport throws. + UnauthorizedError: MockUnauthorizedError, + // Mock the MCP SDK Client Client: class MockClient { setRequestHandler() {} @@ -114,11 +101,6 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ }, })) -// Mock UnauthorizedError in the auth module so instanceof checks work -void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ - UnauthorizedError: MockUnauthorizedError, -})) - beforeEach(() => { transportCalls.length = 0 simulateAuthFlow = true diff --git a/packages/opencode/test/mcp/oauth-browser.test.ts b/packages/opencode/test/mcp/oauth-browser.test.ts index 7cb6525a8..bfa7e3a8a 100644 --- a/packages/opencode/test/mcp/oauth-browser.test.ts +++ b/packages/opencode/test/mcp/oauth-browser.test.ts @@ -36,13 +36,14 @@ class MockUnauthorizedError extends Error { // Track what options were passed to each transport constructor const transportCalls: Array<{ - type: "streamable" | "sse" + type: "streamable" url: string options: { authProvider?: unknown; requestInit?: RequestInit } }> = [] -// Mock the transport constructors -void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ +// v2 packs Client, StreamableHTTPClientTransport, and UnauthorizedError into +// the single package root, so one mock.module covers the whole surface. +void mock.module("@modelcontextprotocol/client", () => ({ StreamableHTTPClientTransport: class MockStreamableHTTP { url: string authProvider: { redirectToAuthorization?: (url: URL) => Promise } | undefined @@ -69,25 +70,8 @@ void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ // Mock successful auth completion } }, -})) - -void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ - SSEClientTransport: class MockSSE { - constructor(url: URL) { - transportCalls.push({ - type: "sse", - url: url.toString(), - options: {}, - }) - } - async start() { - throw new Error("Mock SSE transport cannot connect") - } - }, -})) - -// Mock the MCP SDK Client to trigger OAuth flow -void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ + UnauthorizedError: MockUnauthorizedError, + // Mock the MCP SDK Client to trigger OAuth flow Client: class MockClient { setRequestHandler() {} @@ -101,11 +85,6 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ }, })) -// Mock UnauthorizedError in the auth module -void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ - UnauthorizedError: MockUnauthorizedError, -})) - beforeEach(() => { openShouldFail = false openCalledWith = undefined diff --git a/packages/opencode/test/mcp/protocol.test.ts b/packages/opencode/test/mcp/protocol.test.ts new file mode 100644 index 000000000..3b1e81e53 --- /dev/null +++ b/packages/opencode/test/mcp/protocol.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, mock, test, beforeEach } from "bun:test" +import { Effect, Schema } from "effect" +import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp" +import { testEffect } from "../lib/effect" + +// Captures the options passed to each Client constructor +const clientOptions: Array<{ capabilities?: unknown; versionNegotiation?: { mode?: unknown } }> = [] + +class MockUnauthorizedError extends Error { + constructor() { + super("Unauthorized") + this.name = "UnauthorizedError" + } +} + +class MockStdioTransport { + async start() { + throw new Error("Mock transport cannot connect") + } + async close() {} +} + +// v2 packs Client, StreamableHTTPClientTransport, and UnauthorizedError into +// the single package root, so one mock.module covers the whole surface. The +// Client mock records constructor options and bridges connect() into the +// (throwing) mock transports — enough to assert how config maps to +// versionNegotiation without a real server. +void mock.module("@modelcontextprotocol/client/stdio", () => ({ + StdioClientTransport: MockStdioTransport, +})) + +void mock.module("@modelcontextprotocol/client", () => ({ + StreamableHTTPClientTransport: class MockStreamableHTTP { + async start() { + throw new Error("Mock transport cannot connect") + } + async close() {} + }, + UnauthorizedError: MockUnauthorizedError, + Client: class MockClient { + constructor(_info: unknown, options?: { capabilities?: unknown; versionNegotiation?: { mode?: unknown } }) { + clientOptions.push(options ?? {}) + } + setRequestHandler() {} + async connect(transport: { start: () => Promise }) { + await transport.start() + } + async close() {} + }, +})) + +beforeEach(() => { + clientOptions.length = 0 +}) + +// Import MCP after mocking +const { MCP } = await import("../../src/mcp/index") +const it = testEffect(MCP.defaultLayer) + +describe("mcp.protocol config parsing", () => { + test("accepts auto, legacy, and modern on local and remote entries", () => { + for (const protocol of ["auto", "legacy", "modern"] as const) { + const local = Schema.decodeUnknownSync(ConfigMCPV1.Info)({ + type: "local", + command: ["echo", "test"], + protocol, + }) + expect(local.protocol).toBe(protocol) + + const remote = Schema.decodeUnknownSync(ConfigMCPV1.Info)({ + type: "remote", + url: "https://example.com/mcp", + protocol, + }) + expect(remote.protocol).toBe(protocol) + } + }) + + test("protocol is optional and stays undefined when absent (auto is applied at client construction)", () => { + const local = Schema.decodeUnknownSync(ConfigMCPV1.Info)({ type: "local", command: ["echo", "test"] }) + expect(local.protocol).toBeUndefined() + + const remote = Schema.decodeUnknownSync(ConfigMCPV1.Info)({ type: "remote", url: "https://example.com/mcp" }) + expect(remote.protocol).toBeUndefined() + }) + + test("rejects unrecognized protocol values", () => { + expect(() => + Schema.decodeUnknownSync(ConfigMCPV1.Info)({ type: "local", command: ["echo", "test"], protocol: "2025" }), + ).toThrow() + expect(() => + Schema.decodeUnknownSync(ConfigMCPV1.Info)({ + type: "remote", + url: "https://example.com/mcp", + protocol: "auto-modern", + }), + ).toThrow() + }) +}) + +describe("mcp.versionNegotiation mapping", () => { + it.instance( + "undefined protocol negotiates auto (probe with fallback)", + () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + yield* mcp.add("auto-default", { type: "remote", url: "https://example.com/mcp" }).pipe( + Effect.catch(() => Effect.void), + ) + + expect(clientOptions.length).toBe(1) + expect(clientOptions[0].versionNegotiation).toEqual({ mode: "auto" }) + }), + { config: { mcp: {} } }, + ) + + it.instance( + "protocol auto negotiates auto", + () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + yield* mcp.add("auto-explicit", { + type: "remote", + url: "https://example.com/mcp", + protocol: "auto", + }).pipe(Effect.catch(() => Effect.void)) + + expect(clientOptions.length).toBe(1) + expect(clientOptions[0].versionNegotiation).toEqual({ mode: "auto" }) + }), + { config: { mcp: {} } }, + ) + + it.instance( + "protocol legacy skips the probe and runs the 2025 initialize handshake", + () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + yield* mcp.add("legacy-server", { + type: "remote", + url: "https://example.com/mcp", + protocol: "legacy", + }).pipe(Effect.catch(() => Effect.void)) + + expect(clientOptions.length).toBe(1) + expect(clientOptions[0].versionNegotiation).toEqual({ mode: "legacy" }) + }), + { config: { mcp: {} } }, + ) + + it.instance( + "protocol modern pins 2026-07-28 with no fallback", + () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + yield* mcp.add("modern-server", { + type: "remote", + url: "https://example.com/mcp", + protocol: "modern", + }).pipe(Effect.catch(() => Effect.void)) + + expect(clientOptions.length).toBe(1) + expect(clientOptions[0].versionNegotiation).toEqual({ mode: { pin: "2026-07-28" } }) + }), + { config: { mcp: {} } }, + ) + + it.instance( + "local entries map protocol the same way", + () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + yield* mcp.add("modern-local", { + type: "local", + command: ["echo", "test"], + protocol: "modern", + }).pipe(Effect.catch(() => Effect.void)) + + expect(clientOptions.length).toBe(1) + expect(clientOptions[0].versionNegotiation).toEqual({ mode: { pin: "2026-07-28" } }) + }), + { config: { mcp: {} } }, + ) + + it.instance( + "client capabilities stay elicitation + roots regardless of protocol", + () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + yield* mcp.add("caps-server", { + type: "remote", + url: "https://example.com/mcp", + protocol: "legacy", + }).pipe(Effect.catch(() => Effect.void)) + + expect(clientOptions.length).toBe(1) + expect(clientOptions[0].capabilities).toEqual({ elicitation: {}, roots: {} }) + }), + { config: { mcp: {} } }, + ) +}) diff --git a/packages/opencode/test/mcp/session-recovery.test.ts b/packages/opencode/test/mcp/session-recovery.test.ts index 658650822..a93de6d1d 100644 --- a/packages/opencode/test/mcp/session-recovery.test.ts +++ b/packages/opencode/test/mcp/session-recovery.test.ts @@ -2,7 +2,7 @@ import path from "node:path" import { describe, expect, test } from "bun:test" describe("mcp session recovery", () => { - test("reinitializes and retries once after a session-bound POST returns 404", async () => { + test("session-bound POST returning 404 surfaces a typed transport error (v2 removed auto session recovery)", async () => { const child = Bun.spawn([process.execPath, path.join(import.meta.dir, "../fixture/mcp-session-recovery.ts")], { cwd: path.join(import.meta.dir, "../.."), stdout: "pipe", @@ -15,13 +15,16 @@ describe("mcp session recovery", () => { ]) expect(code, stderr).toBe(0) - expect(JSON.parse(stdout)).toEqual([ - { method: "initialize", session: null }, - { method: "notifications/initialized", session: "expired" }, - { method: "ping", session: "expired" }, - { method: "initialize", session: null }, - { method: "notifications/initialized", session: "replacement" }, - { method: "ping", session: "replacement" }, - ]) + // v1's StreamableHTTPClientTransport transparently re-initialized a fresh + // session and retried once on 404. v2 removed that recovery: the ping + // rejects with SdkHttpError and no second initialize is attempted. + expect(JSON.parse(stdout)).toEqual({ + posts: [ + { method: "initialize", session: null }, + { method: "notifications/initialized", session: "expired" }, + { method: "ping", session: "expired" }, + ], + error: { name: "SdkHttpError", code: "CLIENT_HTTP_NOT_IMPLEMENTED", status: 404 }, + }) }) }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 5ae49b8e0..ff7c1db68 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1918,6 +1918,7 @@ export type McpLocalConfig = { } enabled?: boolean timeout?: number + protocol?: "auto" | "legacy" | "modern" } export type McpOAuthConfig = { @@ -1946,6 +1947,7 @@ export type McpRemoteConfig = { */ oauth?: McpOAuthConfig | false timeout?: number + protocol?: "auto" | "legacy" | "modern" } /** diff --git a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch b/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch deleted file mode 100644 index ae2520287..000000000 --- a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch +++ /dev/null @@ -1,427 +0,0 @@ -diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts -index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644 ---- a/dist/cjs/client/index.d.ts -+++ b/dist/cjs/client/index.d.ts -@@ -428,6 +428,8 @@ export declare class Client>; -+ callTool(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise>; - callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{ - [x: string]: unknown; - content: ({ -diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts -index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644 ---- a/dist/esm/client/index.d.ts -+++ b/dist/esm/client/index.d.ts -@@ -428,6 +428,8 @@ export declare class Client>; -+ callTool(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise>; - callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{ - [x: string]: unknown; - content: ({ -diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js -index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c101584c84 100644 ---- a/dist/cjs/client/index.js -+++ b/dist/cjs/client/index.js -@@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol { - } - async connect(transport, options) { - await super.connect(transport); -+ transport.onsessionexpired = async () => { -+ await this._initialize(transport); -+ }; - // When transport sessionId is already set this means we are trying to reconnect. - // In this case we don't need to initialize again. - if (transport.sessionId !== undefined) { - return; - } - try { -- const result = await this.request({ -- method: 'initialize', -- params: { -- protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION, -- capabilities: this._capabilities, -- clientInfo: this._clientInfo -- } -- }, types_js_1.InitializeResultSchema, options); -- if (result === undefined) { -- throw new Error(`Server sent invalid initialize result: ${result}`); -- } -- if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { -- throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); -- } -- this._serverCapabilities = result.capabilities; -- this._serverVersion = result.serverInfo; -- // HTTP transports must set the protocol version in each header after initialization. -- if (transport.setProtocolVersion) { -- transport.setProtocolVersion(result.protocolVersion); -- } -- this._instructions = result.instructions; -- await this.notification({ -- method: 'notifications/initialized' -- }); -- // Set up list changed handlers now that we know server capabilities -- if (this._pendingListChangedConfig) { -- this._setupListChangedHandlers(this._pendingListChangedConfig); -- this._pendingListChangedConfig = undefined; -- } -+ await this._initialize(transport, options); - } - catch (error) { - // Disconnect if initialization fails. -@@ -330,6 +305,37 @@ class Client extends protocol_js_1.Protocol { - throw error; - } - } -+ async _initialize(transport, options) { -+ const result = await this.request({ -+ method: 'initialize', -+ params: { -+ protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION, -+ capabilities: this._capabilities, -+ clientInfo: this._clientInfo -+ } -+ }, types_js_1.InitializeResultSchema, options); -+ if (result === undefined) { -+ throw new Error(`Server sent invalid initialize result: ${result}`); -+ } -+ if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { -+ throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); -+ } -+ this._serverCapabilities = result.capabilities; -+ this._serverVersion = result.serverInfo; -+ // HTTP transports must set the protocol version in each header after initialization. -+ if (transport.setProtocolVersion) { -+ transport.setProtocolVersion(result.protocolVersion); -+ } -+ this._instructions = result.instructions; -+ await this.notification({ -+ method: 'notifications/initialized' -+ }); -+ // Set up list changed handlers now that we know server capabilities -+ if (this._pendingListChangedConfig) { -+ this._setupListChangedHandlers(this._pendingListChangedConfig); -+ this._pendingListChangedConfig = undefined; -+ } -+ } - /** - * After initialization has completed, this will be populated with the server's reported capabilities. - */ -diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js -index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644 ---- a/dist/cjs/client/streamableHttp.js -+++ b/dist/cjs/client/streamableHttp.js -@@ -290,7 +290,38 @@ class StreamableHTTPClientTransport { - this.onclose?.(); - } - async send(message, options) { -+ return this._send(message, options, false); -+ } -+ async _recoverSession(expiredSessionId) { -+ if (this._sessionRecovery) { -+ await this._sessionRecovery; -+ return true; -+ } -+ if (this._sessionId !== expiredSessionId) -+ return true; -+ this._sessionId = undefined; -+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.()); - try { -+ await this._sessionRecovery; -+ } -+ catch (error) { -+ this._sessionId = undefined; -+ await this.close(); -+ throw error; -+ } -+ finally { -+ this._sessionRecovery = undefined; -+ } -+ return true; -+ } -+ async _send(message, options, isSessionRetry) { -+ try { -+ if (this._sessionRecovery && !(0, types_js_1.isInitializeRequest)(message) && !(0, types_js_1.isInitializedNotification)(message)) { -+ await this._sessionRecovery; -+ if (options?.isRequestActive?.() === false) { -+ throw new Error('Request is no longer active'); -+ } -+ } - const { resumptionToken, onresumptiontoken } = options || {}; - if (resumptionToken) { - // If we have at last event ID, we need to reconnect the SSE stream -@@ -298,6 +329,7 @@ class StreamableHTTPClientTransport { - return; - } - const headers = await this._commonHeaders(); -+ const requestSessionId = headers.get('mcp-session-id') ?? undefined; - headers.set('content-type', 'application/json'); - headers.set('accept', 'application/json, text/event-stream'); - const init = { -@@ -310,11 +342,20 @@ class StreamableHTTPClientTransport { - const response = await (this._fetch ?? fetch)(this._url, init); - // Handle session ID received during initialization - const sessionId = response.headers.get('mcp-session-id'); -- if (sessionId) { -+ if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) { - this._sessionId = sessionId; - } - if (!response.ok) { - const text = await response.text().catch(() => null); -+ if (response.status === 404 && requestSessionId && !isSessionRetry && !(0, types_js_1.isInitializedNotification)(message)) { -+ const recovered = await this._recoverSession(requestSessionId); -+ if (options?.isRequestActive?.() === false) { -+ throw new Error('Request is no longer active'); -+ } -+ if (recovered) { -+ return this._send(message, options, true); -+ } -+ } - if (response.status === 401 && this._authProvider) { - // Prevent infinite recursion when server returns 401 after successful auth - if (this._hasCompletedAuthFlow) { -@@ -335,7 +376,7 @@ class StreamableHTTPClientTransport { - // Mark that we completed auth flow - this._hasCompletedAuthFlow = true; - // Purposely _not_ awaited, so we don't call onerror twice -- return this.send(message); -+ return this._send(message, options, isSessionRetry); - } - if (response.status === 403 && this._authProvider) { - const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response); -@@ -362,7 +403,7 @@ class StreamableHTTPClientTransport { - if (result !== 'AUTHORIZED') { - throw new auth_js_1.UnauthorizedError(); - } -- return this.send(message); -+ return this._send(message, options, isSessionRetry); - } - } - throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`); -diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js -index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb77538182b 100644 ---- a/dist/cjs/shared/protocol.js -+++ b/dist/cjs/shared/protocol.js -@@ -744,7 +744,12 @@ class Protocol { - } - else { - // No related task - send through transport normally -- this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => { -+ this._transport.send(jsonrpcRequest, { -+ relatedRequestId, -+ resumptionToken, -+ onresumptiontoken, -+ isRequestActive: () => this._responseHandlers.has(messageId) -+ }).catch(error => { - this._cleanupTimeout(messageId); - reject(error); - }); -diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js -index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8eb9e4caff 100644 ---- a/dist/esm/client/index.js -+++ b/dist/esm/client/index.js -@@ -284,41 +284,16 @@ export class Client extends Protocol { - } - async connect(transport, options) { - await super.connect(transport); -+ transport.onsessionexpired = async () => { -+ await this._initialize(transport); -+ }; - // When transport sessionId is already set this means we are trying to reconnect. - // In this case we don't need to initialize again. - if (transport.sessionId !== undefined) { - return; - } - try { -- const result = await this.request({ -- method: 'initialize', -- params: { -- protocolVersion: LATEST_PROTOCOL_VERSION, -- capabilities: this._capabilities, -- clientInfo: this._clientInfo -- } -- }, InitializeResultSchema, options); -- if (result === undefined) { -- throw new Error(`Server sent invalid initialize result: ${result}`); -- } -- if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { -- throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); -- } -- this._serverCapabilities = result.capabilities; -- this._serverVersion = result.serverInfo; -- // HTTP transports must set the protocol version in each header after initialization. -- if (transport.setProtocolVersion) { -- transport.setProtocolVersion(result.protocolVersion); -- } -- this._instructions = result.instructions; -- await this.notification({ -- method: 'notifications/initialized' -- }); -- // Set up list changed handlers now that we know server capabilities -- if (this._pendingListChangedConfig) { -- this._setupListChangedHandlers(this._pendingListChangedConfig); -- this._pendingListChangedConfig = undefined; -- } -+ await this._initialize(transport, options); - } - catch (error) { - // Disconnect if initialization fails. -@@ -326,6 +301,37 @@ export class Client extends Protocol { - throw error; - } - } -+ async _initialize(transport, options) { -+ const result = await this.request({ -+ method: 'initialize', -+ params: { -+ protocolVersion: LATEST_PROTOCOL_VERSION, -+ capabilities: this._capabilities, -+ clientInfo: this._clientInfo -+ } -+ }, InitializeResultSchema, options); -+ if (result === undefined) { -+ throw new Error(`Server sent invalid initialize result: ${result}`); -+ } -+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { -+ throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); -+ } -+ this._serverCapabilities = result.capabilities; -+ this._serverVersion = result.serverInfo; -+ // HTTP transports must set the protocol version in each header after initialization. -+ if (transport.setProtocolVersion) { -+ transport.setProtocolVersion(result.protocolVersion); -+ } -+ this._instructions = result.instructions; -+ await this.notification({ -+ method: 'notifications/initialized' -+ }); -+ // Set up list changed handlers now that we know server capabilities -+ if (this._pendingListChangedConfig) { -+ this._setupListChangedHandlers(this._pendingListChangedConfig); -+ this._pendingListChangedConfig = undefined; -+ } -+ } - /** - * After initialization has completed, this will be populated with the server's reported capabilities. - */ -diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js -index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644 ---- a/dist/esm/client/streamableHttp.js -+++ b/dist/esm/client/streamableHttp.js -@@ -1,5 +1,5 @@ - import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js'; --import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; -+import { isInitializedNotification, isInitializeRequest, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; - import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js'; - import { EventSourceParserStream } from 'eventsource-parser/stream'; - // Default reconnection options for StreamableHTTP connections -@@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport { - this.onclose?.(); - } - async send(message, options) { -+ return this._send(message, options, false); -+ } -+ async _recoverSession(expiredSessionId) { -+ if (this._sessionRecovery) { -+ await this._sessionRecovery; -+ return true; -+ } -+ if (this._sessionId !== expiredSessionId) -+ return true; -+ this._sessionId = undefined; -+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.()); - try { -+ await this._sessionRecovery; -+ } -+ catch (error) { -+ this._sessionId = undefined; -+ await this.close(); -+ throw error; -+ } -+ finally { -+ this._sessionRecovery = undefined; -+ } -+ return true; -+ } -+ async _send(message, options, isSessionRetry) { -+ try { -+ if (this._sessionRecovery && !isInitializeRequest(message) && !isInitializedNotification(message)) { -+ await this._sessionRecovery; -+ if (options?.isRequestActive?.() === false) { -+ throw new Error('Request is no longer active'); -+ } -+ } - const { resumptionToken, onresumptiontoken } = options || {}; - if (resumptionToken) { - // If we have at last event ID, we need to reconnect the SSE stream -@@ -294,6 +325,7 @@ export class StreamableHTTPClientTransport { - return; - } - const headers = await this._commonHeaders(); -+ const requestSessionId = headers.get('mcp-session-id') ?? undefined; - headers.set('content-type', 'application/json'); - headers.set('accept', 'application/json, text/event-stream'); - const init = { -@@ -306,11 +338,20 @@ export class StreamableHTTPClientTransport { - const response = await (this._fetch ?? fetch)(this._url, init); - // Handle session ID received during initialization - const sessionId = response.headers.get('mcp-session-id'); -- if (sessionId) { -+ if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) { - this._sessionId = sessionId; - } - if (!response.ok) { - const text = await response.text().catch(() => null); -+ if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitializedNotification(message)) { -+ const recovered = await this._recoverSession(requestSessionId); -+ if (options?.isRequestActive?.() === false) { -+ throw new Error('Request is no longer active'); -+ } -+ if (recovered) { -+ return this._send(message, options, true); -+ } -+ } - if (response.status === 401 && this._authProvider) { - // Prevent infinite recursion when server returns 401 after successful auth - if (this._hasCompletedAuthFlow) { -@@ -331,7 +372,7 @@ export class StreamableHTTPClientTransport { - // Mark that we completed auth flow - this._hasCompletedAuthFlow = true; - // Purposely _not_ awaited, so we don't call onerror twice -- return this.send(message); -+ return this._send(message, options, isSessionRetry); - } - if (response.status === 403 && this._authProvider) { - const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response); -@@ -358,7 +399,7 @@ export class StreamableHTTPClientTransport { - if (result !== 'AUTHORIZED') { - throw new UnauthorizedError(); - } -- return this.send(message); -+ return this._send(message, options, isSessionRetry); - } - } - throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`); -diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js -index bfa2b7120a0f50c569364ea5264e6f811076f44f..abd8dfd707c155f71dae7aeeeeaf7547368ac749 100644 ---- a/dist/esm/shared/protocol.js -+++ b/dist/esm/shared/protocol.js -@@ -740,7 +740,12 @@ export class Protocol { - } - else { - // No related task - send through transport normally -- this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => { -+ this._transport.send(jsonrpcRequest, { -+ relatedRequestId, -+ resumptionToken, -+ onresumptiontoken, -+ isRequestActive: () => this._responseHandlers.has(messageId) -+ }).catch(error => { - this._cleanupTimeout(messageId); - reject(error); - }); From 20ef2e837d1abc55ef91656786e266a86d4a9474 Mon Sep 17 00:00:00 2001 From: Lex Date: Wed, 26 Aug 2026 09:49:40 +0800 Subject: [PATCH 4/6] chore: record delivery binding for expose-negotiated-protocol --- .specgit.yaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.specgit.yaml b/.specgit.yaml index 1a76cc0ea..76dea78f0 100644 --- a/.specgit.yaml +++ b/.specgit.yaml @@ -1,9 +1,7 @@ version: 1 -delivery: upgrade-mcp-client +delivery: expose-negotiated-protocol context: kind: branch - branch: refactor/447-upgrade-mcp-client + branch: feat/450-expose-negotiated-protocol issues: - - 447 - - 448 -pr: 449 + - 450 From 38b7294d97c16f808118b4028f20f688ed260cbe Mon Sep 17 00:00:00 2001 From: Lex Date: Wed, 26 Aug 2026 09:50:03 +0800 Subject: [PATCH 5/6] chore: record delivery binding for expose-negotiated-protocol --- .specgit.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.specgit.yaml b/.specgit.yaml index 76dea78f0..deb427936 100644 --- a/.specgit.yaml +++ b/.specgit.yaml @@ -5,3 +5,4 @@ context: branch: feat/450-expose-negotiated-protocol issues: - 450 +pr: 451 From 21f4754d4baba18128cc84dd85df1ad489383705 Mon Sep 17 00:00:00 2001 From: Lex Date: Wed, 26 Aug 2026 10:10:18 +0800 Subject: [PATCH 6/6] feat(mcp): expose negotiated protocol era and version in server status --- packages/opencode/src/cli/cmd/mcp.ts | 2 +- packages/opencode/src/mcp/index.ts | 27 +++- .../opencode/test/mcp/protocol-era.test.ts | 138 ++++++++++++++++++ .../test/server/httpapi-exercise/index.ts | 24 ++- packages/sdk/js/src/v2/gen/types.gen.ts | 2 + 5 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 packages/opencode/test/mcp/protocol-era.test.ts diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 26090e105..eb84652e0 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -139,7 +139,7 @@ export const McpListCommand = effectCmd({ statusText = "not initialized" } else if (status.status === "connected") { statusIcon = "✓" - statusText = "connected" + statusText = `connected${status.era ? ` · ${status.era}` : ""}${status.protocolVersion ? ` (${status.protocolVersion})` : ""}` if (hasOAuth && hasStoredTokens) { hint = " (OAuth)" } diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 91d253a83..2bbdcf95c 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -80,7 +80,26 @@ function createClient(directory: string, protocol?: "auto" | "legacy" | "modern" return client } -const StatusConnected = Schema.Struct({ status: Schema.Literal("connected") }).annotate({ +// Protocol versions are ISO dates and sort lexicographically, so >= the +// 2026-07-28 baseline (server/discover era) means modern. +export function protocolEra(version: string): "modern" | "legacy" { + return version >= "2026-07-28" ? "modern" : "legacy" +} + +// Connected status carries the negotiated protocol era/version when the client +// reports one; test doubles without the getter get the plain shape. +function connectedStatus(client: MCPClient): Status { + const protocolVersion = + typeof client.getNegotiatedProtocolVersion === "function" ? client.getNegotiatedProtocolVersion() : undefined + if (protocolVersion === undefined) return { status: "connected" } + return { status: "connected", era: protocolEra(protocolVersion), protocolVersion } +} + +const StatusConnected = Schema.Struct({ + status: Schema.Literal("connected"), + era: Schema.optional(Schema.Literals(["modern", "legacy"])), + protocolVersion: Schema.optional(Schema.String), +}).annotate({ identifier: "MCPStatusConnected", }) const StatusDisabled = Schema.Struct({ status: Schema.Literal("disabled") }).annotate({ @@ -308,7 +327,7 @@ export const layer = Layer.effect( return Effect.void }), ) - if (result) return { client: result, status: { status: "connected" } as Status } + if (result) return { client: result, status: connectedStatus(result) } return { client: undefined as MCPClient | undefined, @@ -339,7 +358,7 @@ export const layer = Layer.effect( return yield* connectTransport(transport, connectTimeout, mcp.protocol).pipe( Effect.map((client): { client: MCPClient | undefined; status: Status } => ({ client, - status: { status: "connected" }, + status: connectedStatus(client), })), Effect.catch((error): Effect.Effect<{ client: MCPClient | undefined; status: Status }> => { const msg = error instanceof Error ? error.message : String(error) @@ -561,7 +580,7 @@ export const layer = Layer.effect( ) { const bridge = yield* EffectBridge.make() const previous = s.clients[name] - s.status[name] = { status: "connected" } + s.status[name] = connectedStatus(client) s.clients[name] = client s.defs[name] = listed if (instructions) s.instructions[name] = instructions diff --git a/packages/opencode/test/mcp/protocol-era.test.ts b/packages/opencode/test/mcp/protocol-era.test.ts new file mode 100644 index 000000000..e6c56dc4b --- /dev/null +++ b/packages/opencode/test/mcp/protocol-era.test.ts @@ -0,0 +1,138 @@ +import { describe, expect, mock, test, beforeEach } from "bun:test" +import { Effect } from "effect" +import { testEffect } from "../lib/effect" + +// Negotiated protocol version reported by every MockClient; controlled per test. +let negotiatedVersion: string | undefined + +class MockUnauthorizedError extends Error { + constructor() { + super("Unauthorized") + this.name = "UnauthorizedError" + } +} + +// Successful stdio transport: start() resolves, so connect() completes and a +// connected status is built — the seam this file exercises. +class MockStdioTransport { + async start() {} + async close() {} +} + +// v2 packs Client, StreamableHTTPClientTransport, and UnauthorizedError into +// the single package root, so one mock.module covers the whole surface. The +// Client mock bridges connect() into the (succeeding) mock transport and +// reports the per-test negotiated version. +void mock.module("@modelcontextprotocol/client/stdio", () => ({ + StdioClientTransport: MockStdioTransport, +})) + +void mock.module("@modelcontextprotocol/client", () => ({ + StreamableHTTPClientTransport: class MockStreamableHTTP { + async start() {} + async close() {} + }, + UnauthorizedError: MockUnauthorizedError, + Client: class MockClient { + onclose: (() => void) | undefined + setRequestHandler() {} + setNotificationHandler() {} + getServerCapabilities() { + return undefined + } + getInstructions() { + return undefined + } + getNegotiatedProtocolVersion() { + return negotiatedVersion + } + async connect(transport: { start: () => Promise }) { + await transport.start() + } + async close() {} + }, +})) + +beforeEach(() => { + negotiatedVersion = undefined +}) + +// Import MCP after mocking +const { MCP } = await import("../../src/mcp/index") +const it = testEffect(MCP.defaultLayer) + +describe("mcp.protocolEra derivation", () => { + test("versions at or above the 2026-07-28 baseline are modern", () => { + expect(MCP.protocolEra("2026-07-28")).toBe("modern") + expect(MCP.protocolEra("2026-08-15")).toBe("modern") + expect(MCP.protocolEra("2027-01-05")).toBe("modern") + }) + + test("2025-era versions are legacy", () => { + expect(MCP.protocolEra("2025-06-18")).toBe("legacy") + expect(MCP.protocolEra("2025-11-25")).toBe("legacy") + expect(MCP.protocolEra("2026-07-27")).toBe("legacy") + }) +}) + +describe("mcp connected status era population", () => { + it.instance( + "modern negotiated version surfaces era and protocolVersion", + () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + negotiatedVersion = "2026-07-28" + yield* mcp.add("era-modern", { type: "local", command: ["echo", "test"] }) + const statuses = yield* mcp.status() + expect(statuses["era-modern"]).toEqual({ + status: "connected", + era: "modern", + protocolVersion: "2026-07-28", + }) + }), + { config: { mcp: {} } }, + ) + + it.instance( + "legacy negotiated version surfaces legacy era", + () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + negotiatedVersion = "2025-06-18" + yield* mcp.add("era-legacy", { type: "local", command: ["echo", "test"] }) + const statuses = yield* mcp.status() + expect(statuses["era-legacy"]).toEqual({ + status: "connected", + era: "legacy", + protocolVersion: "2025-06-18", + }) + }), + { config: { mcp: {} } }, + ) + + it.instance( + "undefined negotiated version omits both fields", + () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + yield* mcp.add("era-unreported", { type: "local", command: ["echo", "test"] }) + const statuses = yield* mcp.status() + expect(statuses["era-unreported"]).toEqual({ status: "connected" }) + }), + { config: { mcp: {} } }, + ) + + it.instance( + "status endpoint reflects the stored era fields", + () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + negotiatedVersion = "2026-07-28" + yield* mcp.add("era-status", { type: "local", command: ["echo", "test"] }) + const statuses = yield* mcp.status() + expect(statuses["era-status"]?.status).toBe("connected") + expect(statuses["era-status"]).toMatchObject({ era: "modern", protocolVersion: "2026-07-28" }) + }), + { config: { mcp: {} } }, + ) +}) diff --git a/packages/opencode/test/server/httpapi-exercise/index.ts b/packages/opencode/test/server/httpapi-exercise/index.ts index 842399295..5b9ee14d9 100644 --- a/packages/opencode/test/server/httpapi-exercise/index.ts +++ b/packages/opencode/test/server/httpapi-exercise/index.ts @@ -372,7 +372,29 @@ const scenarios: Scenario[] = [ }), "status", ), - http.protected.get("/mcp", "mcp.status").json(), + http.protected.get("/mcp", "mcp.status").json( + 200, + (body) => { + object(body) + for (const entry of Object.values(body)) { + object(entry) + if (entry.status === "connected") { + check( + entry.era === undefined || entry.era === "modern" || entry.era === "legacy", + "connected MCP status era should be modern or legacy when present", + ) + check( + entry.protocolVersion === undefined || typeof entry.protocolVersion === "string", + "connected MCP status protocolVersion should be a string when present", + ) + } else { + check(entry.era === undefined, "MCP era field should only appear on connected statuses") + check(entry.protocolVersion === undefined, "MCP protocolVersion field should only appear on connected statuses") + } + } + }, + "status", + ), http.protected .post("/mcp", "mcp.add") .mutating() diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index ff7c1db68..c74b13d39 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2466,6 +2466,8 @@ export type FormatterStatus = { export type McpStatusConnected = { status: "connected" + era?: "modern" | "legacy" + protocolVersion?: string } export type McpStatusDisabled = {