浏览代码

fix(mcp): restore legacy SDK compatibility (#39373)

Aiden Cline 1 周之前
父节点
当前提交
982a9044c5
共有 31 个文件被更改,包括 1080 次插入594 次删除
  1. 66 17
      bun.lock
  2. 1 1
      bunfig.toml
  3. 2 2
      package.json
  4. 1 2
      packages/opencode/package.json
  5. 102 45
      packages/opencode/src/cli/cmd/mcp.ts
  6. 0 4
      packages/opencode/src/mcp/auth.ts
  7. 91 44
      packages/opencode/src/mcp/catalog.ts
  8. 30 52
      packages/opencode/src/mcp/index.ts
  9. 3 9
      packages/opencode/src/mcp/oauth-callback.ts
  10. 22 63
      packages/opencode/src/mcp/oauth-provider.ts
  11. 0 1
      packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts
  12. 1 1
      packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts
  13. 1 1
      packages/opencode/src/session/tools.ts
  14. 23 2
      packages/opencode/src/tool/code-mode.ts
  15. 4 3
      packages/opencode/test/fixture/mcp-lifecycle-stdio.ts
  16. 5 9
      packages/opencode/test/fixture/mcp-session-recovery.ts
  17. 15 51
      packages/opencode/test/mcp/catalog.test.ts
  18. 4 21
      packages/opencode/test/mcp/headers.test.ts
  19. 18 13
      packages/opencode/test/mcp/lifecycle.test.ts
  20. 5 3
      packages/opencode/test/mcp/oauth-auto-connect.test.ts
  21. 4 2
      packages/opencode/test/mcp/oauth-browser.test.ts
  22. 1 1
      packages/opencode/test/mcp/oauth-callback.test.ts
  23. 41 0
      packages/opencode/test/mcp/oauth-provider.test.ts
  24. 0 20
      packages/opencode/test/mcp/session-recovery.test.ts
  25. 9 8
      packages/opencode/test/tool/code-mode-integration.test.ts
  26. 1 1
      packages/opencode/test/tool/code-mode.test.ts
  27. 1 1
      packages/opencode/test/tool/registry.test.ts
  28. 0 2
      packages/sdk/js/src/v2/gen/sdk.gen.ts
  29. 0 1
      packages/sdk/js/src/v2/gen/types.gen.ts
  30. 0 214
      patches/@modelcontextprotocol%2Fclient@2.0.0.patch
  31. 629 0
      patches/@modelcontextprotocol%2Fsdk@1.29.0.patch

+ 66 - 17
bun.lock

@@ -592,7 +592,7 @@
         "@effect/platform-node": "catalog:",
         "@ff-labs/fff-bun": "0.9.4",
         "@gitlab/opencode-gitlab-auth": "1.3.3",
-        "@modelcontextprotocol/client": "2.0.0",
+        "@modelcontextprotocol/sdk": "1.29.0",
         "@octokit/graphql": "9.0.2",
         "@octokit/rest": "catalog:",
         "@openauthjs/openauth": "catalog:",
@@ -666,7 +666,6 @@
       },
       "devDependencies": {
         "@babel/core": "7.28.4",
-        "@modelcontextprotocol/server": "2.0.0",
         "@octokit/webhooks-types": "7.6.1",
         "@opencode-ai/core": "workspace:*",
         "@opencode-ai/http-recorder": "workspace:*",
@@ -1073,8 +1072,8 @@
     "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
     "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
     "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.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",
-    "@modelcontextprotocol/client@2.0.0": "patches/@modelcontextprotocol%2Fclient@2.0.0.patch",
     "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
     "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
     "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
@@ -1676,6 +1675,8 @@
 
     "@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="],
 
+    "@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=="],
@@ -1830,11 +1831,7 @@
 
     "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
 
-    "@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=="],
+    "@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=="],
 
     "@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=="],
 
@@ -3394,7 +3391,7 @@
 
     "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
 
-    "content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
+    "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
 
     "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
 
@@ -3406,6 +3403,8 @@
 
     "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=="],
@@ -3710,6 +3709,8 @@
 
     "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.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="],
 
     "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
@@ -4094,6 +4095,8 @@
 
     "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=="],
@@ -5002,6 +5005,8 @@
 
     "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=="],
@@ -5950,13 +5955,15 @@
 
     "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
 
-    "@modelcontextprotocol/client/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
+    "@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/client/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
+    "@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="],
 
-    "@modelcontextprotocol/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
+    "@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
 
-    "@modelcontextprotocol/server/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
+    "@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=="],
 
     "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
 
@@ -6232,8 +6239,6 @@
 
     "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=="],
@@ -6316,8 +6321,6 @@
 
     "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=="],
@@ -6466,6 +6469,8 @@
 
     "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=="],
@@ -6794,28 +6799,58 @@
 
     "@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=="],
@@ -6828,10 +6863,14 @@
 
     "@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=="],
@@ -7340,6 +7379,10 @@
 
     "@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=="],
@@ -7348,14 +7391,20 @@
 
     "@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/core/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],

+ 1 - 1
bunfig.toml

@@ -2,7 +2,7 @@
 exact = true
 # Only install newly resolved package versions published at least 3 days ago.
 minimumReleaseAge = 259200
-minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@modelcontextprotocol/client", "@modelcontextprotocol/core", "@modelcontextprotocol/server", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"]
+minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"]
 
 [test]
 root = "./do-not-run-tests-from-root"

+ 2 - 2
package.json

@@ -155,8 +155,8 @@
     "pacote@21.5.0": "patches/pacote@21.5.0.patch",
     "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.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",
     "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
-    "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
-    "@modelcontextprotocol/client@2.0.0": "patches/@modelcontextprotocol%2Fclient@2.0.0.patch"
+    "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
   }
 }

+ 1 - 2
packages/opencode/package.json

@@ -30,7 +30,6 @@
   },
   "devDependencies": {
     "@babel/core": "7.28.4",
-    "@modelcontextprotocol/server": "2.0.0",
     "@octokit/webhooks-types": "7.6.1",
     "@opencode-ai/core": "workspace:*",
     "@opencode-ai/http-recorder": "workspace:*",
@@ -81,7 +80,7 @@
     "@effect/platform-node": "catalog:",
     "@ff-labs/fff-bun": "0.9.4",
     "@gitlab/opencode-gitlab-auth": "1.3.3",
-    "@modelcontextprotocol/client": "2.0.0",
+    "@modelcontextprotocol/sdk": "1.29.0",
     "@octokit/graphql": "9.0.2",
     "@octokit/rest": "catalog:",
     "@openauthjs/openauth": "catalog:",

+ 102 - 45
packages/opencode/src/cli/cmd/mcp.ts

@@ -2,10 +2,13 @@ import { cmd } from "./cmd"
 import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import { effectCmd } from "../effect-cmd"
 import { Cause } from "effect"
-import { Client, StreamableHTTPClientTransport, UnauthorizedError } from "@modelcontextprotocol/client"
+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 * as prompts from "@clack/prompts"
 import { UI } from "../ui"
-import { CLIENT_OPTIONS, MCP } from "../../mcp"
+import { MCP } from "../../mcp"
 import { McpAuth } from "../../mcp/auth"
 import { McpOAuthProvider } from "../../mcp/oauth-provider"
 import { Config } from "@/config/config"
@@ -728,53 +731,107 @@ export const McpDebugCommand = effectCmd({
       const spinner = prompts.spinner()
       spinner.start("Testing connection...")
 
-      const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
-      let authorizationUrl: URL | undefined
-      const authProvider = new McpOAuthProvider(
-        serverName,
-        serverConfig.url,
-        {
-          clientId: oauthConfig?.clientId,
-          clientSecret: oauthConfig?.clientSecret,
-          scope: oauthConfig?.scope,
-          callbackPort: oauthConfig?.callbackPort,
-          redirectUri: oauthConfig?.redirectUri,
-        },
-        {
-          onRedirect: async (url) => {
-            authorizationUrl = url
+      // Test basic HTTP connectivity first
+      try {
+        const response = await fetch(serverConfig.url, {
+          method: "POST",
+          headers: {
+            ...serverConfig.headers,
+            "Content-Type": "application/json",
+            Accept: "application/json, text/event-stream",
           },
-        },
-        auth,
-      )
-      const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), {
-        authProvider,
-        requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined,
-      })
-      const client = new Client({ name: "opencode-debug", version: InstallationVersion }, CLIENT_OPTIONS)
+          body: JSON.stringify({
+            jsonrpc: "2.0",
+            method: "initialize",
+            params: {
+              protocolVersion: LATEST_PROTOCOL_VERSION,
+              capabilities: {},
+              clientInfo: { name: "opencode-debug", version: InstallationVersion },
+            },
+            id: 1,
+          }),
+        })
 
-      try {
-        await client.connect(transport)
-        spinner.stop("SDK connection successful")
-        prompts.log.success(
-          `Connected using MCP ${client.getNegotiatedProtocolVersion() ?? "unknown"} (${client.getProtocolEra() ?? "unknown"})`,
-        )
-        const serverInfo = client.getServerVersion()
-        if (serverInfo) prompts.log.info(`Server info: ${JSON.stringify(serverInfo)}`)
-      } catch (error) {
-        if (error instanceof UnauthorizedError) {
-          spinner.stop("OAuth required")
-          prompts.log.info(`OAuth flow triggered: ${error.message}`)
-          if (authorizationUrl) prompts.log.info(`Authorization URL: ${authorizationUrl}`)
-          const clientInfo = await authProvider.clientInformation()
-          if (clientInfo) prompts.log.info(`Client ID available: ${clientInfo.client_id}`)
-          if (!clientInfo) prompts.log.info("No client ID - dynamic registration will be attempted")
+        spinner.stop(`HTTP response: ${response.status} ${response.statusText}`)
+
+        // Check for WWW-Authenticate header
+        const wwwAuth = response.headers.get("www-authenticate")
+        if (wwwAuth) {
+          prompts.log.info(`WWW-Authenticate: ${wwwAuth}`)
+        }
+
+        if (response.status === 401) {
+          prompts.log.info("Initial unauthenticated check returned 401, so this server requires OAuth")
+
+          // Try to discover OAuth metadata
+          const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
+          const authProvider = new McpOAuthProvider(
+            serverName,
+            serverConfig.url,
+            {
+              clientId: oauthConfig?.clientId,
+              clientSecret: oauthConfig?.clientSecret,
+              scope: oauthConfig?.scope,
+              redirectUri: oauthConfig?.redirectUri,
+            },
+            {
+              onRedirect: async () => {},
+            },
+            auth,
+          )
+
+          prompts.log.info("Testing OAuth flow (without completing authorization)...")
+
+          // Try creating transport with auth provider to trigger discovery
+          const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), {
+            authProvider,
+            requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined,
+          })
+
+          try {
+            const client = new Client({
+              name: "opencode-debug",
+              version: InstallationVersion,
+            })
+            await client.connect(transport)
+            prompts.log.success("Connection successful (already authenticated)")
+            await client.close()
+          } catch (error) {
+            if (error instanceof UnauthorizedError) {
+              prompts.log.info(`OAuth flow triggered: ${error.message}`)
+
+              // Check if dynamic registration would be attempted
+              const clientInfo = await authProvider.clientInformation()
+              if (clientInfo) {
+                prompts.log.info(`Client ID available: ${clientInfo.client_id}`)
+              } else {
+                prompts.log.info("No client ID - dynamic registration will be attempted")
+              }
+            } else {
+              prompts.log.error(`Connection error: ${error instanceof Error ? error.message : String(error)}`)
+            }
+          }
+        } else if (response.status >= 200 && response.status < 300) {
+          prompts.log.success("Server responded successfully (no auth required or already authenticated)")
+          const body = await response.text()
+          try {
+            const json = JSON.parse(body)
+            if (json.result?.serverInfo) {
+              prompts.log.info(`Server info: ${JSON.stringify(json.result.serverInfo)}`)
+            }
+          } catch {
+            // Not JSON, ignore
+          }
         } else {
-          spinner.stop("Connection failed", 1)
-          prompts.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
+          prompts.log.warn(`Unexpected status: ${response.status}`)
+          const body = await response.text().catch(() => "")
+          if (body) {
+            prompts.log.info(`Response body: ${body.substring(0, 500)}`)
+          }
         }
-      } finally {
-        await client.close().catch(() => {})
+      } catch (error) {
+        spinner.stop("Connection failed", 1)
+        prompts.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
       }
 
       prompts.outro("Debug complete")

+ 0 - 4
packages/opencode/src/mcp/auth.ts

@@ -11,7 +11,6 @@ export const Tokens = Schema.Struct({
   refreshToken: Schema.mutableKey(Schema.optional(Schema.String)),
   expiresAt: Schema.mutableKey(Schema.optional(Schema.Number)),
   scope: Schema.mutableKey(Schema.optional(Schema.String)),
-  issuer: Schema.mutableKey(Schema.optional(Schema.String)),
 })
 export type Tokens = Schema.Schema.Type<typeof Tokens>
 
@@ -20,9 +19,6 @@ export const ClientInfo = Schema.Struct({
   clientSecret: Schema.mutableKey(Schema.optional(Schema.String)),
   clientIdIssuedAt: Schema.mutableKey(Schema.optional(Schema.Number)),
   clientSecretExpiresAt: Schema.mutableKey(Schema.optional(Schema.Number)),
-  redirectUris: Schema.mutableKey(Schema.optional(Schema.Array(Schema.String))),
-  issuer: Schema.mutableKey(Schema.optional(Schema.String)),
-  configPreRegistered: Schema.mutableKey(Schema.optional(Schema.Boolean)),
 })
 export type ClientInfo = Schema.Schema.Type<typeof ClientInfo>
 

+ 91 - 44
packages/opencode/src/mcp/catalog.ts

@@ -1,57 +1,77 @@
-import { Client, type CallToolResult, type Tool as MCPToolDef } from "@modelcontextprotocol/client"
+import { Client } from "@modelcontextprotocol/sdk/client/index.js"
+import {
+  CallToolResultSchema,
+  ListToolsResultSchema,
+  ToolSchema,
+  type Tool as MCPToolDef,
+} from "@modelcontextprotocol/sdk/types.js"
 import { dynamicTool, jsonSchema, type JSONSchema7, type Tool } from "ai"
 import { Effect } from "effect"
 
 const DEFAULT_TIMEOUT = 30_000
+const MAX_LIST_PAGES = 1_000
 
-export interface McpTool {
-  readonly def: MCPToolDef
-  readonly client: Client
-  readonly timeout?: number
-}
+const TolerantListToolsResultSchema = ListToolsResultSchema.extend({
+  tools: ToolSchema.omit({ outputSchema: true }).array(),
+})
 
-export async function callTool(
-  tool: McpTool,
-  args: Record<string, unknown>,
-  signal?: AbortSignal,
-): Promise<CallToolResult> {
-  const result = await tool.client.callTool(
-    { name: tool.def.name, arguments: args },
-    {
-      resetTimeoutOnProgress: true,
-      signal,
-      timeout: tool.timeout,
-      // The MCP SDK only sends a progress token when this hook is present, enabling timeout resets.
-      onprogress: () => {},
-    },
-  )
-  if (result.isError)
-    throw new Error(
-      result.content
-        .flatMap((item) => (item.type === "text" ? [item.text] : []))
-        .filter((text) => text.trim())
-        .join("\n\n") || "MCP tool returned an error",
-    )
-  return result
+export async function paginate<T, R extends { nextCursor?: string }>(
+  list: (cursor?: string) => Promise<R>,
+  items: (result: R) => T[],
+) {
+  const result: T[] = []
+  const cursors = new Set<string>()
+  let cursor: string | undefined
+
+  for (let page = 0; page < MAX_LIST_PAGES; page++) {
+    const page = await list(cursor)
+    result.push(...items(page))
+    if (page.nextCursor === undefined) return result
+    if (cursors.has(page.nextCursor)) throw new Error(`MCP list returned duplicate cursor: ${page.nextCursor}`)
+    cursors.add(page.nextCursor)
+    cursor = page.nextCursor
+  }
+
+  throw new Error(`MCP list exceeded ${MAX_LIST_PAGES} pages`)
 }
 
 export function defs(client: Client, timeout?: number) {
   return listTools(client, timeout ?? DEFAULT_TIMEOUT).pipe(Effect.catch(() => Effect.void))
 }
 
-export function convertTool(tool: McpTool): Tool {
+export function convertTool(mcpTool: MCPToolDef, client: Client, timeout?: number): Tool {
   const inputSchema: JSONSchema7 = {
-    ...(tool.def.inputSchema as JSONSchema7),
+    ...(mcpTool.inputSchema as JSONSchema7),
     type: "object",
-    properties: (tool.def.inputSchema.properties ?? {}) as JSONSchema7["properties"],
+    properties: (mcpTool.inputSchema.properties ?? {}) as JSONSchema7["properties"],
     additionalProperties: false,
   }
 
   return dynamicTool({
-    description: tool.def.description ?? "",
+    description: mcpTool.description ?? "",
     inputSchema: jsonSchema(inputSchema),
     execute: async (args: unknown, options) => {
-      const result = await callTool(tool, (args || {}) as Record<string, unknown>, options.abortSignal)
+      const result = await client.callTool(
+        {
+          name: mcpTool.name,
+          arguments: (args || {}) as Record<string, unknown>,
+        },
+        CallToolResultSchema,
+        {
+          resetTimeoutOnProgress: true,
+          signal: options.abortSignal,
+          timeout,
+          // The MCP SDK only sends a progress token when this hook is present, enabling timeout resets.
+          onprogress: () => {},
+        },
+      )
+      if (result.isError)
+        throw new Error(
+          result.content
+            .flatMap((item) => (item.type === "text" ? [item.text] : []))
+            .filter((text) => text.trim())
+            .join("\n\n") || "MCP tool returned an error",
+        )
       if (result.content.length > 0 || result.structuredContent === undefined || result.structuredContent === null)
         return result
       return {
@@ -98,26 +118,53 @@ export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_")
 
 export const toolName = (clientName: string, name: string) => sanitize(clientName) + "_" + sanitize(name)
 
-export async function prompts(client: Client, timeout?: number) {
-  if (!client.getServerCapabilities()?.prompts) return []
-  return (await client.listPrompts(undefined, { timeout })).prompts
+export function prompts(client: Client, timeout?: number) {
+  if (!client.getServerCapabilities()?.prompts) return Promise.resolve([])
+  return paginate(
+    (cursor) => client.listPrompts(cursor === undefined ? undefined : { cursor }, { timeout }),
+    (result) => result.prompts,
+  )
 }
 
-export async function resources(client: Client, timeout?: number) {
-  if (!client.getServerCapabilities()?.resources) return []
-  return (await client.listResources(undefined, { timeout })).resources
+export function resources(client: Client, timeout?: number) {
+  if (!client.getServerCapabilities()?.resources) return Promise.resolve([])
+  return paginate(
+    (cursor) => client.listResources(cursor === undefined ? undefined : { cursor }, { timeout }),
+    (result) => result.resources,
+  )
 }
 
-export async function resourceTemplates(client: Client, timeout?: number) {
-  if (!client.getServerCapabilities()?.resources) return []
-  return (await client.listResourceTemplates(undefined, { timeout })).resourceTemplates
+export function resourceTemplates(client: Client, timeout?: number) {
+  if (!client.getServerCapabilities()?.resources) return Promise.resolve([])
+  return paginate(
+    (cursor) => client.listResourceTemplates(cursor === undefined ? undefined : { cursor }, { timeout }),
+    (result) => result.resourceTemplates,
+  )
 }
 
 function listTools(client: Client, timeout: number) {
   return Effect.tryPromise({
-    try: async () => (await client.listTools(undefined, { timeout })).tools,
+    try: () =>
+      paginate(
+        async (cursor) => {
+          const params = cursor === undefined ? undefined : { cursor }
+          try {
+            return await client.listTools(params, { timeout })
+          } catch (error) {
+            if (!(error instanceof Error) || !isOutputSchemaValidationError(error)) throw error
+            return client.request({ method: "tools/list", params }, TolerantListToolsResultSchema, { timeout })
+          }
+        },
+        (result) => result.tools,
+      ),
     catch: (error) => (error instanceof Error ? error : new Error(String(error))),
   })
 }
 
+function isOutputSchemaValidationError(error: Error) {
+  return /can't resolve reference|resolves to more than one schema|outputSchema|schema.*reference|reference.*schema/i.test(
+    error.message,
+  )
+}
+
 export * as McpCatalog from "./catalog"

+ 30 - 52
packages/opencode/src/mcp/index.ts

@@ -3,18 +3,18 @@ import { pathToFileURL } from "node:url"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 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 {
-  Client,
-  type ClientOptions,
-  StreamableHTTPClientTransport,
-  SSEClientTransport,
-  UnauthorizedError,
-  RegistrationRejectedError,
-  SdkHttpError,
+  ListRootsRequestSchema,
   type LoggingMessageNotification,
+  LoggingMessageNotificationSchema,
   type Tool as MCPToolDef,
-} from "@modelcontextprotocol/client"
-import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"
+  ToolListChangedNotificationSchema,
+} from "@modelcontextprotocol/sdk/types.js"
 import { Config } from "@/config/config"
 import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp"
 import { NamedError } from "@opencode-ai/core/util/error"
@@ -36,7 +36,7 @@ import { McpEvent } from "@opencode-ai/schema/mcp-event"
 import { McpBrowser } from "./browser"
 
 const DEFAULT_TIMEOUT = 30_000
-export const CLIENT_OPTIONS = {
+const CLIENT_OPTIONS = {
   capabilities: {
     // https://github.com/anomalyco/opencode/issues/11948
     // sampling: {},
@@ -47,8 +47,6 @@ export const CLIENT_OPTIONS = {
     // https://github.com/anomalyco/opencode/issues/28567
     // tasks: {},
   },
-  versionNegotiation: { mode: "auto" },
-  listMaxPages: 1_000,
 } satisfies ClientOptions
 
 export const Resource = Schema.Struct({
@@ -72,19 +70,13 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP
   name: Schema.String,
 }) {}
 
-type MCPClient = Client & { onToolsChanged?: (error: Error | null) => void }
+type MCPClient = Client
 
 function createClient(directory: string) {
-  const client: MCPClient = new Client(
-    { name: "opencode", version: InstallationVersion },
-    {
-      ...CLIENT_OPTIONS,
-      listChanged: {
-        tools: { autoRefresh: false, onChanged: (error) => client.onToolsChanged?.(error) },
-      },
-    },
+  const client = new Client({ name: "opencode", version: InstallationVersion }, CLIENT_OPTIONS)
+  client.setRequestHandler(ListRootsRequestSchema, () =>
+    Promise.resolve({ roots: [{ uri: pathToFileURL(directory).href }] }),
   )
-  client.setRequestHandler("roots/list", async () => ({ roots: [{ uri: pathToFileURL(directory).href }] }))
   return client
 }
 
@@ -162,7 +154,12 @@ export interface ServerInstructions {
 }
 
 /** An MCP tool in its native shape; consumers adapt it to their own tool format. */
-export type McpTool = McpCatalog.McpTool
+export interface McpTool {
+  /** Shared cached definition; consumers must copy rather than mutate it. */
+  readonly def: MCPToolDef
+  readonly client: MCPClient
+  readonly timeout?: number
+}
 
 export interface Interface {
   readonly status: () => Effect.Effect<Record<string, Status>>
@@ -193,11 +190,7 @@ export interface Interface {
     mcpName: string,
     onAuthorization?: (authorizationUrl: string) => void,
   ) => Effect.Effect<Status, NotFoundError>
-  readonly finishAuth: (
-    mcpName: string,
-    authorizationCode: string,
-    iss?: string,
-  ) => Effect.Effect<Status, NotFoundError>
+  readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect<Status, NotFoundError>
   readonly removeAuth: (mcpName: string) => Effect.Effect<void>
   readonly supportsOAuth: (mcpName: string) => Effect.Effect<boolean, NotFoundError>
   readonly hasStoredTokens: (mcpName: string) => Effect.Effect<boolean>
@@ -298,18 +291,11 @@ const layer = Layer.effect(
           Effect.map((client) => ({ client, transportName: name })),
           Effect.catch((error) => {
             const lastError = error instanceof Error ? error : new Error(String(error))
-            const registrationRejected =
-              error instanceof RegistrationRejectedError ||
-              lastError.message.includes("registration") ||
-              lastError.message.includes("client_id")
             const isAuthError =
-              error instanceof UnauthorizedError ||
-              registrationRejected ||
-              (authProvider && error instanceof SdkHttpError && error.status === 401) ||
-              (authProvider && lastError.message.includes("OAuth"))
+              error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth"))
 
             if (isAuthError) {
-              if (registrationRejected) {
+              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.",
@@ -468,16 +454,12 @@ const layer = Layer.effect(
         )
       }
 
-      client.setNotificationHandler("notifications/message", (notification) =>
+      client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) =>
         bridge.promise(serverLog(name, notification.params)),
       )
 
       if (!client.getServerCapabilities()?.tools) return
-      client.onToolsChanged = async (error) => {
-        if (error) {
-          await bridge.promise(Effect.logWarning("failed to refresh MCP tools", { server: name, error: error.message }))
-          return
-        }
+      client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
         if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
 
         const listed = await bridge.promise(McpCatalog.defs(client, timeout))
@@ -486,7 +468,7 @@ const layer = Layer.effect(
 
         s.defs[name] = listed
         await bridge.promise(events.publish(ToolsChanged, { server: name }).pipe(Effect.ignore))
-      }
+      })
     }
 
     function serverLog(name: string, params: LoggingMessageNotification["params"]) {
@@ -922,7 +904,7 @@ const layer = Layer.effect(
         }),
       )
 
-      const callback = yield* Effect.promise(() => callbackPromise)
+      const code = yield* Effect.promise(() => callbackPromise)
 
       const storedState = yield* auth.getOAuthState(mcpName)
       if (storedState !== result.oauthState) {
@@ -930,20 +912,16 @@ const layer = Layer.effect(
         throw new Error("OAuth state mismatch - potential CSRF attack")
       }
       yield* auth.clearOAuthState(mcpName)
-      return yield* finishAuth(mcpName, callback.code, callback.iss)
+      return yield* finishAuth(mcpName, code)
     })
 
-    const finishAuth = Effect.fn("MCP.finishAuth")(function* (
-      mcpName: string,
-      authorizationCode: string,
-      iss?: string,
-    ) {
+    const finishAuth = Effect.fn("MCP.finishAuth")(function* (mcpName: string, authorizationCode: string) {
       yield* requireMcpConfig(mcpName)
       const pending = pendingOAuthTransports.get(mcpName)
       if (!pending) throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`)
 
       const error = yield* Effect.tryPromise({
-        try: () => pending.transport.finishAuth(authorizationCode, iss),
+        try: () => pending.transport.finishAuth(authorizationCode),
         catch: (error) => error,
       }).pipe(
         Effect.match({

+ 3 - 9
packages/opencode/src/mcp/oauth-callback.ts

@@ -9,13 +9,8 @@ const OAUTH_CALLBACK_HOST = "127.0.0.1"
 let currentPort = OAUTH_CALLBACK_PORT
 let currentPath = OAUTH_CALLBACK_PATH
 
-export interface AuthorizationCallback {
-  code: string
-  iss?: string
-}
-
 interface PendingAuth {
-  resolve: (callback: AuthorizationCallback) => void
+  resolve: (code: string) => void
   reject: (error: Error) => void
   timeout: ReturnType<typeof setTimeout>
 }
@@ -54,7 +49,6 @@ function handleRequest(req: import("http").IncomingMessage, res: import("http").
   }
 
   const code = url.searchParams.get("code")
-  const iss = url.searchParams.get("iss") ?? undefined
   const state = url.searchParams.get("state")
   const error = url.searchParams.get("error")
   const errorDescription = url.searchParams.get("error_description")
@@ -101,7 +95,7 @@ function handleRequest(req: import("http").IncomingMessage, res: import("http").
   clearTimeout(pending.timeout)
   pendingAuths.delete(state)
   cleanupStateIndex(state)
-  pending.resolve({ code, iss })
+  pending.resolve(code)
 
   res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
   res.end(OauthCallbackPage.success({ provider: "MCP" }))
@@ -136,7 +130,7 @@ export async function ensureRunning(redirectUri?: string): Promise<void> {
   })
 }
 
-export function waitForCallback(oauthState: string, mcpName?: string): Promise<AuthorizationCallback> {
+export function waitForCallback(oauthState: string, mcpName?: string): Promise<string> {
   if (mcpName) mcpNameToState.set(mcpName, oauthState)
   return new Promise((resolve, reject) => {
     const timeout = setTimeout(() => {

+ 22 - 63
packages/opencode/src/mcp/oauth-provider.ts

@@ -1,9 +1,10 @@
+import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
 import type {
-  OAuthClientProvider,
   OAuthClientMetadata,
-  StoredOAuthTokens,
-  StoredOAuthClientInformation,
-} from "@modelcontextprotocol/client"
+  OAuthTokens,
+  OAuthClientInformation,
+  OAuthClientInformationFull,
+} from "@modelcontextprotocol/sdk/shared/auth.js"
 import { Effect } from "effect"
 import { McpAuth } from "./auth"
 
@@ -22,14 +23,6 @@ export interface McpOAuthCallbacks {
   onRedirect: (url: URL) => void | Promise<void>
 }
 
-function registrationMetadata(info: StoredOAuthClientInformation) {
-  return {
-    clientIdIssuedAt: "client_id_issued_at" in info ? info.client_id_issued_at : undefined,
-    clientSecretExpiresAt: "client_secret_expires_at" in info ? info.client_secret_expires_at : undefined,
-    redirectUris: "redirect_uris" in info ? info.redirect_uris : undefined,
-  }
-}
-
 export class McpOAuthProvider implements OAuthClientProvider {
   constructor(
     protected mcpName: string,
@@ -59,21 +52,18 @@ export class McpOAuthProvider implements OAuthClientProvider {
     }
   }
 
-  async clientInformation(): Promise<StoredOAuthClientInformation | undefined> {
-    const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
+  async clientInformation(): Promise<OAuthClientInformation | undefined> {
     if (this.config.clientId) {
-      const issuer = entry?.clientInfo?.clientId === this.config.clientId ? entry.clientInfo.issuer : undefined
       return {
         client_id: this.config.clientId,
         client_secret: this.config.clientSecret,
-        ...(issuer !== undefined ? { issuer } : {}),
       }
     }
 
     // Check stored client info (from dynamic registration)
     // Use getForUrl to validate credentials are for the current server URL
+    const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
     if (entry?.clientInfo) {
-      if (entry.clientInfo.configPreRegistered) return undefined
       // Check if client secret has expired
       if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
         return undefined
@@ -81,14 +71,6 @@ export class McpOAuthProvider implements OAuthClientProvider {
       return {
         client_id: entry.clientInfo.clientId,
         client_secret: entry.clientInfo.clientSecret,
-        ...(entry.clientInfo.clientIdIssuedAt !== undefined
-          ? { client_id_issued_at: entry.clientInfo.clientIdIssuedAt }
-          : {}),
-        ...(entry.clientInfo.clientSecretExpiresAt !== undefined
-          ? { client_secret_expires_at: entry.clientInfo.clientSecretExpiresAt }
-          : {}),
-        redirect_uris: entry.clientInfo.redirectUris ? [...entry.clientInfo.redirectUris] : [this.redirectUrl],
-        ...(entry.clientInfo.issuer !== undefined ? { issuer: entry.clientInfo.issuer } : {}),
       }
     }
 
@@ -96,36 +78,22 @@ export class McpOAuthProvider implements OAuthClientProvider {
     return undefined
   }
 
-  async saveClientInformation(info: StoredOAuthClientInformation): Promise<void> {
-    if (this.config.clientId && info.client_id === this.config.clientId) {
-      await Effect.runPromise(
-        this.auth.updateClientInfo(
-          this.mcpName,
-          { clientId: info.client_id, issuer: info.issuer, configPreRegistered: true },
-          this.serverUrl,
-        ),
-      )
-      return
-    }
-
-    const metadata = registrationMetadata(info)
+  async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
     await Effect.runPromise(
       this.auth.updateClientInfo(
         this.mcpName,
         {
           clientId: info.client_id,
           clientSecret: info.client_secret,
-          clientIdIssuedAt: metadata.clientIdIssuedAt,
-          clientSecretExpiresAt: metadata.clientSecretExpiresAt,
-          redirectUris: metadata.redirectUris ? [...metadata.redirectUris] : [this.redirectUrl],
-          issuer: info.issuer,
+          clientIdIssuedAt: info.client_id_issued_at,
+          clientSecretExpiresAt: info.client_secret_expires_at,
         },
         this.serverUrl,
       ),
     )
   }
 
-  async tokens(): Promise<StoredOAuthTokens | undefined> {
+  async tokens(): Promise<OAuthTokens | undefined> {
     // Use getForUrl to validate tokens are for the current server URL
     const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
     if (!entry?.tokens) return undefined
@@ -138,20 +106,18 @@ export class McpOAuthProvider implements OAuthClientProvider {
         ? Math.max(0, Math.floor(entry.tokens.expiresAt - Date.now() / 1000))
         : undefined,
       scope: entry.tokens.scope,
-      issuer: entry.tokens.issuer,
     }
   }
 
-  async saveTokens(tokens: StoredOAuthTokens): Promise<void> {
+  async saveTokens(tokens: OAuthTokens): Promise<void> {
     await Effect.runPromise(
       this.auth.updateTokens(
         this.mcpName,
         {
           accessToken: tokens.access_token,
           refreshToken: tokens.refresh_token,
-          expiresAt: tokens.expires_in !== undefined ? Date.now() / 1000 + tokens.expires_in : undefined,
+          expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined,
           scope: tokens.scope,
-          issuer: tokens.issuer,
         },
         this.serverUrl,
       ),
@@ -215,10 +181,10 @@ export class McpOAuthProvider implements OAuthClientProvider {
 }
 
 export class McpOAuthPendingProvider extends McpOAuthProvider {
-  private pendingClientInfo?: StoredOAuthClientInformation
-  private pendingTokens?: StoredOAuthTokens
+  private pendingClientInfo?: OAuthClientInformationFull
+  private pendingTokens?: OAuthTokens
 
-  override async clientInformation(): Promise<StoredOAuthClientInformation | undefined> {
+  override async clientInformation(): Promise<OAuthClientInformation | undefined> {
     if (!this.config.clientId) return this.pendingClientInfo
     return {
       client_id: this.config.clientId,
@@ -226,15 +192,15 @@ export class McpOAuthPendingProvider extends McpOAuthProvider {
     }
   }
 
-  override async saveClientInformation(info: StoredOAuthClientInformation): Promise<void> {
+  override async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
     this.pendingClientInfo = info
   }
 
-  override async tokens(): Promise<StoredOAuthTokens | undefined> {
+  override async tokens(): Promise<OAuthTokens | undefined> {
     return this.pendingTokens
   }
 
-  override async saveTokens(tokens: StoredOAuthTokens): Promise<void> {
+  override async saveTokens(tokens: OAuthTokens): Promise<void> {
     this.pendingTokens = tokens
   }
 
@@ -245,7 +211,6 @@ export class McpOAuthPendingProvider extends McpOAuthProvider {
 
   async commit(): Promise<void> {
     if (!this.pendingTokens) return
-    const pendingMetadata = this.pendingClientInfo ? registrationMetadata(this.pendingClientInfo) : undefined
     await Effect.runPromise(
       this.auth.set(
         this.mcpName,
@@ -253,22 +218,16 @@ export class McpOAuthPendingProvider extends McpOAuthProvider {
           tokens: {
             accessToken: this.pendingTokens.access_token,
             refreshToken: this.pendingTokens.refresh_token,
-            expiresAt:
-              this.pendingTokens.expires_in !== undefined
-                ? Date.now() / 1000 + this.pendingTokens.expires_in
-                : undefined,
+            expiresAt: this.pendingTokens.expires_in ? Date.now() / 1000 + this.pendingTokens.expires_in : undefined,
             scope: this.pendingTokens.scope,
-            issuer: this.pendingTokens.issuer,
           },
           clientInfo:
             this.pendingClientInfo && !this.config.clientId
               ? {
                   clientId: this.pendingClientInfo.client_id,
                   clientSecret: this.pendingClientInfo.client_secret,
-                  clientIdIssuedAt: pendingMetadata?.clientIdIssuedAt,
-                  clientSecretExpiresAt: pendingMetadata?.clientSecretExpiresAt,
-                  redirectUris: pendingMetadata?.redirectUris ? [...pendingMetadata.redirectUris] : [this.redirectUrl],
-                  issuer: this.pendingClientInfo.issuer,
+                  clientIdIssuedAt: this.pendingClientInfo.client_id_issued_at,
+                  clientSecretExpiresAt: this.pendingClientInfo.client_secret_expires_at,
                 }
               : undefined,
         },

+ 0 - 1
packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts

@@ -20,7 +20,6 @@ export const AuthStartResponse = Schema.Struct({
 })
 export const AuthCallbackPayload = Schema.Struct({
   code: Schema.String,
-  iss: Schema.optional(Schema.String),
 })
 export const AuthRemoveResponse = Schema.Struct({
   success: Schema.Literal(true),

+ 1 - 1
packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts

@@ -38,7 +38,7 @@ export const mcpHandlers = HttpApiBuilder.group(InstanceHttpApi, "mcp", (handler
       payload: typeof AuthCallbackPayload.Type
     }) {
       return yield* mcp
-        .finishAuth(ctx.params.name, ctx.payload.code, ctx.payload.iss)
+        .finishAuth(ctx.params.name, ctx.payload.code)
         .pipe(
           Effect.catchTag("MCP.NotFoundError", (error) =>
             Effect.fail(

+ 1 - 1
packages/opencode/src/session/tools.ts

@@ -388,7 +388,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
   if (flags.experimentalCodeMode) return tools
 
   for (const [key, entry] of Object.entries(yield* mcp.tools())) {
-    const item = McpCatalog.convertTool(entry)
+    const item = McpCatalog.convertTool(entry.def, entry.client, entry.timeout)
     const execute = item.execute
     if (!execute) continue
 

+ 23 - 2
packages/opencode/src/tool/code-mode.ts

@@ -1,5 +1,5 @@
 import * as Tool from "./tool"
-import { type CallToolResult } from "@modelcontextprotocol/client"
+import { CallToolResultSchema, type CallToolResult } from "@modelcontextprotocol/sdk/types.js"
 import { Cause, Effect, Schema } from "effect"
 import { CodeMode, Tool as SandboxTool, toolError } from "@opencode-ai/codemode"
 import { MCP } from "@/mcp"
@@ -145,7 +145,28 @@ const invokeChildTool = Effect.fn("CodeMode.invokeChildTool")(function* (input:
   )
   const result: CallToolResult = yield* Effect.gen(function* () {
     yield* input.ctx.ask({ permission: input.entry.key, metadata: {}, patterns: ["*"], always: ["*"] })
-    return yield* Effect.promise(() => McpCatalog.callTool(input.entry.tool, input.args, input.ctx.abort))
+    // Deliberately mirrors McpCatalog.convertTool's transport call so the MCP service stays free of tool-loop concerns.
+    return yield* Effect.promise(async () => {
+      const raw = await input.entry.tool.client.callTool(
+        { name: input.entry.tool.def.name, arguments: input.args },
+        CallToolResultSchema,
+        {
+          resetTimeoutOnProgress: true,
+          signal: input.ctx.abort,
+          timeout: input.entry.tool.timeout,
+          // The MCP SDK only sends a progress token when this hook is present, enabling timeout resets.
+          onprogress: () => {},
+        },
+      )
+      if (raw.isError)
+        throw new Error(
+          raw.content
+            .flatMap((item) => (item.type === "text" ? [item.text] : []))
+            .filter((text) => text.trim())
+            .join("\n\n") || "MCP tool returned an error",
+        )
+      return raw
+    })
   }).pipe(
     Effect.withSpan("Tool.execute", {
       attributes: {

+ 4 - 3
packages/opencode/test/fixture/mcp-lifecycle-stdio.ts

@@ -1,5 +1,6 @@
-import { Server } from "@modelcontextprotocol/server"
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"
+import { Server } from "@modelcontextprotocol/sdk/server/index.js"
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
+import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
 
 if (process.argv.includes("--hang")) {
   const pidFile = process.env.MCP_LIFECYCLE_PID_FILE
@@ -10,7 +11,7 @@ if (process.argv.includes("--hang")) {
 
 const server = new Server({ name: "mcp-lifecycle-stdio", version: "1.0.0" }, { capabilities: { tools: {} } })
 
-server.setRequestHandler("tools/list", () =>
+server.setRequestHandler(ListToolsRequestSchema, () =>
   Promise.resolve({
     tools: [
       {

+ 5 - 9
packages/opencode/test/fixture/mcp-session-recovery.ts

@@ -1,11 +1,10 @@
-import { Client, LATEST_PROTOCOL_VERSION, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"
+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"
 
 const posts: Array<{ method: string; session: string | null }> = []
-const concurrent = process.env.MCP_RECOVERY_CONCURRENT === "1"
 let initializeCount = 0
 let pingCount = 0
-let replacementStarted!: () => void
-const replacement = new Promise<void>((resolve) => (replacementStarted = resolve))
 const server = Bun.serve({
   port: 0,
   async fetch(request) {
@@ -18,7 +17,6 @@ const server = Bun.serve({
 
     if (message.method === "initialize") {
       initializeCount++
-      if (initializeCount === 2) replacementStarted()
       return Response.json(
         {
           jsonrpc: "2.0",
@@ -36,8 +34,7 @@ const server = Bun.serve({
     if (message.method === "notifications/initialized") return new Response(null, { status: 202 })
 
     pingCount++
-    if (concurrent && pingCount === 2) await replacement
-    if (pingCount <= (concurrent ? 2 : 1)) return new Response("Session not found", { status: 404 })
+    if (pingCount === 1) return new Response("Session not found", { status: 404 })
     return Response.json({ jsonrpc: "2.0", id: message.id, result: {} })
   },
 })
@@ -45,8 +42,7 @@ const client = new Client({ name: "test", version: "1" })
 
 try {
   await client.connect(new StreamableHTTPClientTransport(server.url))
-  if (concurrent) await Promise.all([client.ping(), client.ping()])
-  else await client.ping()
+  await client.ping()
   process.stdout.write(JSON.stringify(posts))
 } finally {
   await client.close()

+ 15 - 51
packages/opencode/test/mcp/catalog.test.ts

@@ -1,6 +1,8 @@
 import { describe, expect, test } from "bun:test"
-import { Client, InMemoryTransport } from "@modelcontextprotocol/client"
-import { Server } from "@modelcontextprotocol/server"
+import { Client } from "@modelcontextprotocol/sdk/client/index.js"
+import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
+import { Server } from "@modelcontextprotocol/sdk/server/index.js"
+import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
 import { McpCatalog } from "@/mcp/catalog"
 import { Effect } from "effect"
 
@@ -28,10 +30,7 @@ describe("McpCatalog.convertTool", () => {
   test("preserves content when structuredContent is also present", async () => {
     const content = [{ type: "image" as const, mimeType: "image/png", data: "AAAA" }]
     const structuredContent = { image: { mimeType: "image/png", data: "AAAA" } }
-    const converted = McpCatalog.convertTool({
-      def: mcpTool(),
-      client: clientReturning({ content, structuredContent }),
-    })
+    const converted = McpCatalog.convertTool(mcpTool(), clientReturning({ content, structuredContent }))
 
     const output = await converted.execute?.({}, options)
 
@@ -40,10 +39,7 @@ describe("McpCatalog.convertTool", () => {
 
   test("falls back to structuredContent only when content is absent", async () => {
     const structuredContent = { results: [{ title: "one" }] }
-    const converted = McpCatalog.convertTool({
-      def: mcpTool(),
-      client: clientReturning({ content: [], structuredContent }),
-    })
+    const converted = McpCatalog.convertTool(mcpTool(), clientReturning({ content: [], structuredContent }))
 
     const output = await converted.execute?.({}, options)
 
@@ -54,52 +50,18 @@ describe("McpCatalog.convertTool", () => {
   })
 })
 
-describe("McpCatalog.callTool", () => {
-  test("forwards the request options", async () => {
-    const controller = new AbortController()
-    let request: unknown
-    let options: unknown
-    const client = {
-      callTool: async (input: unknown, config: unknown) => {
-        request = input
-        options = config
-        return { content: [] }
-      },
-    } as unknown as Client
-
-    await McpCatalog.callTool({ def: mcpTool(), client, timeout: 123 }, { value: true }, controller.signal)
-
-    expect(request).toEqual({ name: "screenshot", arguments: { value: true } })
-    expect(options).toMatchObject({ resetTimeoutOnProgress: true, signal: controller.signal, timeout: 123 })
-    expect(typeof (options as { onprogress?: unknown }).onprogress).toBe("function")
-  })
-
-  test("throws text returned by an MCP tool error", async () => {
-    const client = clientReturning({
-      isError: true,
-      content: [
-        { type: "image", data: "AAAA", mimeType: "image/png" },
-        { type: "text", text: "first" },
-        { type: "text", text: "second" },
-      ],
-    })
-
-    await expect(McpCatalog.callTool({ def: mcpTool(), client }, {})).rejects.toThrow("first\n\nsecond")
-  })
-})
-
 test("preserves output schema validation across paginated tool discovery", async () => {
   const server = new Server({ name: "pagination", version: "1.0.0" }, { capabilities: { tools: {} } })
-  server.setRequestHandler("tools/list", ({ params }) =>
+  server.setRequestHandler(ListToolsRequestSchema, ({ params }) =>
     Promise.resolve(
       params?.cursor === "page-2"
         ? {
             tools: [
               {
                 name: "second",
-                inputSchema: { type: "object" as const },
+                inputSchema: { type: "object" },
                 outputSchema: {
-                  type: "object" as const,
+                  type: "object",
                   properties: { value: { type: "number" } },
                   required: ["value"],
                 },
@@ -110,9 +72,9 @@ test("preserves output schema validation across paginated tool discovery", async
             tools: [
               {
                 name: "first",
-                inputSchema: { type: "object" as const },
+                inputSchema: { type: "object" },
                 outputSchema: {
-                  type: "object" as const,
+                  type: "object",
                   properties: { value: { type: "string" } },
                   required: ["value"],
                 },
@@ -122,7 +84,7 @@ test("preserves output schema validation across paginated tool discovery", async
           },
     ),
   )
-  server.setRequestHandler("tools/call", ({ params }) =>
+  server.setRequestHandler(CallToolRequestSchema, ({ params }) =>
     Promise.resolve({
       content: [],
       structuredContent: { value: params.name === "first" ? 42 : 1 },
@@ -136,7 +98,9 @@ test("preserves output schema validation across paginated tool discovery", async
   try {
     const tools = await Effect.runPromise(McpCatalog.defs(client))
     expect(tools?.map((tool) => tool.name)).toEqual(["first", "second"])
-    await expect(client.callTool({ name: "first", arguments: {} })).rejects.toThrow(/output schema/i)
+    await expect(client.callTool({ name: "first", arguments: {} })).rejects.toThrow(
+      "Structured content does not match the tool's output schema",
+    )
   } finally {
     await Promise.all([client.close(), server.close()])
   }

+ 4 - 21
packages/opencode/test/mcp/headers.test.ts

@@ -1,5 +1,7 @@
 import { describe, expect } from "bun:test"
-import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server"
+import { Server } from "@modelcontextprotocol/sdk/server/index.js"
+import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
+import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect } from "effect"
 import { testEffect } from "../lib/effect"
@@ -11,7 +13,7 @@ const serve = Effect.acquireRelease(
   Effect.promise(async () => {
     const requests: Headers[] = []
     const protocol = new Server({ name: "headers", version: "1.0.0" }, { capabilities: { tools: {} } })
-    protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] }))
+    protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] }))
     const transport = new WebStandardStreamableHTTPServerTransport({
       sessionIdGenerator: () => crypto.randomUUID(),
       enableJsonResponse: true,
@@ -36,11 +38,6 @@ const serve = Effect.acquireRelease(
   (server) => Effect.promise(server.close),
 )
 
-const serveUnauthorized = Effect.acquireRelease(
-  Effect.sync(() => Bun.serve({ port: 0, fetch: () => new Response("Unauthorized", { status: 401 }) })),
-  (server) => Effect.sync(() => server.stop(true)),
-)
-
 describe("mcp.headers", () => {
   it.instance("headers are passed to transports when oauth is enabled (default)", () =>
     Effect.gen(function* () {
@@ -102,18 +99,4 @@ describe("mcp.headers", () => {
       }
     }),
   )
-
-  it.instance("reports 401 as failed when oauth is explicitly disabled", () =>
-    Effect.gen(function* () {
-      const server = yield* serveUnauthorized
-      const mcp = yield* MCP.Service
-      const result = yield* mcp.add("unauthorized-no-oauth", {
-        type: "remote",
-        url: server.url.toString(),
-        oauth: false,
-      })
-
-      expect(result.status).toMatchObject({ "unauthorized-no-oauth": { status: "failed" } })
-    }),
-  )
 })

+ 18 - 13
packages/opencode/test/mcp/lifecycle.test.ts

@@ -1,12 +1,18 @@
 import path from "node:path"
 import { pathToFileURL } from "node:url"
 import { expect } from "bun:test"
+import { Server } from "@modelcontextprotocol/sdk/server/index.js"
+import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
 import {
-  Server,
-  WebStandardStreamableHTTPServerTransport,
+  GetPromptRequestSchema,
+  ListPromptsRequestSchema,
+  ListResourcesRequestSchema,
+  ListResourceTemplatesRequestSchema,
+  ListToolsRequestSchema,
+  ReadResourceRequestSchema,
   type ServerCapabilities,
   type Tool,
-} from "@modelcontextprotocol/server"
+} from "@modelcontextprotocol/sdk/types.js"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Cause, Effect, Exit } from "effect"
 import type { MCP as MCPNS } from "../../src/mcp/index"
@@ -60,35 +66,35 @@ function lifecycleServer(input?: { capabilities?: ServerCapabilities; instructio
         })
 
         if (capabilities.tools) {
-          protocol.setRequestHandler("tools/list", (request) => {
+          protocol.setRequestHandler(ListToolsRequestSchema, (request) => {
             if (state.listToolsError) throw new Error(state.listToolsError)
             const page = state.toolPages?.[request.params?.cursor ?? "initial"]
             return Promise.resolve({ tools: page?.items ?? state.tools, nextCursor: page?.nextCursor })
           })
         }
         if (capabilities.prompts) {
-          protocol.setRequestHandler("prompts/list", (request) => {
+          protocol.setRequestHandler(ListPromptsRequestSchema, (request) => {
             const page = state.promptPages?.[request.params?.cursor ?? "initial"]
             return Promise.resolve({ prompts: page?.items ?? state.prompts, nextCursor: page?.nextCursor })
           })
-          protocol.setRequestHandler("prompts/get", async () => {
+          protocol.setRequestHandler(GetPromptRequestSchema, async () => {
             if (state.requestDelay) await Bun.sleep(state.requestDelay)
             return { messages: [{ role: "user", content: { type: "text", text: "prompt result" } }] }
           })
         }
         if (capabilities.resources) {
-          protocol.setRequestHandler("resources/list", (request) => {
+          protocol.setRequestHandler(ListResourcesRequestSchema, (request) => {
             const page = state.resourcePages?.[request.params?.cursor ?? "initial"]
             return Promise.resolve({ resources: page?.items ?? state.resources, nextCursor: page?.nextCursor })
           })
-          protocol.setRequestHandler("resources/templates/list", (request) => {
+          protocol.setRequestHandler(ListResourceTemplatesRequestSchema, (request) => {
             const page = state.resourceTemplatePages?.[request.params?.cursor ?? "initial"]
             return Promise.resolve({
               resourceTemplates: page?.items ?? state.resourceTemplates,
               nextCursor: page?.nextCursor,
             })
           })
-          protocol.setRequestHandler("resources/read", async (request) => {
+          protocol.setRequestHandler(ReadResourceRequestSchema, async (request) => {
             if (state.requestDelay) await Bun.sleep(state.requestDelay)
             return { contents: [{ uri: request.params.uri, text: "resource result" }] }
           })
@@ -139,7 +145,7 @@ function hangingLifecycleServer() {
   return Effect.acquireRelease(
     Effect.promise(async () => {
       const protocol = new Server({ name: "mcp-lifecycle-hanging", version: "1.0.0" }, { capabilities: { tools: {} } })
-      protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] }))
+      protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] }))
       const transport = new WebStandardStreamableHTTPServerTransport({
         sessionIdGenerator: () => crypto.randomUUID(),
         enableJsonResponse: true,
@@ -278,7 +284,7 @@ it.instance("follows cursors when listing tools, prompts, resources, and templat
   }),
 )
 
-it.instance("accepts empty cursors and terminates on repeated cursors", () =>
+it.instance("accepts empty cursors and rejects repeated cursors", () =>
   Effect.gen(function* () {
     const empty = yield* lifecycleServer({ capabilities: { prompts: {} } })
     empty.state.promptPages = {
@@ -295,8 +301,7 @@ it.instance("accepts empty cursors and terminates on repeated cursors", () =>
     const result = yield* mcp.add("looping-cursor", remote(looping.url))
 
     expect(Object.keys(yield* mcp.prompts())).toEqual(["empty-cursor:prompt-one", "empty-cursor:prompt-two"])
-    expect(statusName(result.status, "looping-cursor")).toBe("connected")
-    expect(Object.keys(yield* mcp.tools())).toEqual([])
+    expect(statusName(result.status, "looping-cursor")).toBe("failed")
   }),
 )
 

+ 5 - 3
packages/opencode/test/mcp/oauth-auto-connect.test.ts

@@ -1,5 +1,7 @@
 import { expect } from "bun:test"
-import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server"
+import { Server } from "@modelcontextprotocol/sdk/server/index.js"
+import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
+import { ListResourcesRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -38,13 +40,13 @@ function serveOAuthMcp(options: OAuthMcpOptions = {}) {
       let requiresAuth = true
 
       if (capabilities === "tools") {
-        protocol.setRequestHandler("tools/list", () => {
+        protocol.setRequestHandler(ListToolsRequestSchema, () => {
           listToolsCalls++
           return Promise.resolve({ tools: [{ name: "test_tool", inputSchema: { type: "object" } }] })
         })
       }
       if (capabilities === "resources") {
-        protocol.setRequestHandler("resources/list", () =>
+        protocol.setRequestHandler(ListResourcesRequestSchema, () =>
           Promise.resolve({ resources: [{ name: "docs", uri: "docs://readme" }] }),
         )
       }

+ 4 - 2
packages/opencode/test/mcp/oauth-browser.test.ts

@@ -1,5 +1,7 @@
 import { expect } from "bun:test"
-import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server"
+import { Server } from "@modelcontextprotocol/sdk/server/index.js"
+import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
+import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Deferred, Effect, Layer, Option } from "effect"
 import { Config } from "../../src/config/config"
@@ -39,7 +41,7 @@ const serveOAuthMcp = Effect.acquireRelease(
   Effect.promise(async () => {
     const requests: Array<{ pathname: string; headers: Headers }> = []
     const protocol = new Server({ name: "oauth-browser", version: "1.0.0" }, { capabilities: { tools: {} } })
-    protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] }))
+    protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] }))
     const transport = new WebStandardStreamableHTTPServerTransport({
       sessionIdGenerator: () => crypto.randomUUID(),
       enableJsonResponse: true,

+ 1 - 1
packages/opencode/test/mcp/oauth-callback.test.ts

@@ -74,7 +74,7 @@ describe("McpOAuthCallback.ensureRunning", () => {
     const response = await fetch(`${redirectUri}?code=code&state=success`)
 
     expect(response.status).toBe(200)
-    expect(await callback).toEqual({ code: "code", iss: undefined })
+    expect(await callback).toBe("code")
     expect(McpOAuthCallback.isRunning()).toBe(false)
   })
 

+ 41 - 0
packages/opencode/test/mcp/oauth-provider.test.ts

@@ -1,4 +1,5 @@
 import { test, expect, describe } from "bun:test"
+import { determineScope } from "@modelcontextprotocol/sdk/client/auth.js"
 import { McpOAuthProvider, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "../../src/mcp/oauth-provider"
 import type { McpAuth } from "../../src/mcp/auth"
 
@@ -59,3 +60,43 @@ describe("McpOAuthProvider.clientMetadata", () => {
     expect(provider.clientMetadata.token_endpoint_auth_method).toBe("none")
   })
 })
+
+describe("MCP OAuth scope selection", () => {
+  test("adds offline_access when the authorization server and client support refresh tokens", () => {
+    expect(
+      determineScope({
+        resourceMetadata: {
+          resource: "https://mcp.example.com/mcp",
+          scopes_supported: ["resource.read"],
+        },
+        authServerMetadata: {
+          issuer: "https://auth.example.com",
+          authorization_endpoint: "https://auth.example.com/authorize",
+          token_endpoint: "https://auth.example.com/token",
+          response_types_supported: ["code"],
+          scopes_supported: ["resource.read", "offline_access"],
+        },
+        clientMetadata: makeProvider({}).clientMetadata,
+      }),
+    ).toBe("resource.read offline_access")
+  })
+
+  test("does not add unsupported authorization server scopes", () => {
+    expect(
+      determineScope({
+        resourceMetadata: {
+          resource: "https://mcp.example.com/mcp",
+          scopes_supported: ["resource.read"],
+        },
+        authServerMetadata: {
+          issuer: "https://auth.example.com",
+          authorization_endpoint: "https://auth.example.com/authorize",
+          token_endpoint: "https://auth.example.com/token",
+          response_types_supported: ["code"],
+          scopes_supported: ["resource.read"],
+        },
+        clientMetadata: makeProvider({}).clientMetadata,
+      }),
+    ).toBe("resource.read")
+  })
+})

+ 0 - 20
packages/opencode/test/mcp/session-recovery.test.ts

@@ -24,24 +24,4 @@ describe("mcp session recovery", () => {
       { method: "ping", session: "replacement" },
     ])
   })
-
-  test("retries a concurrent stale response after recovery completes", async () => {
-    const child = Bun.spawn([process.execPath, path.join(import.meta.dir, "../fixture/mcp-session-recovery.ts")], {
-      cwd: path.join(import.meta.dir, "../.."),
-      env: { ...process.env, MCP_RECOVERY_CONCURRENT: "1" },
-      stdout: "pipe",
-      stderr: "pipe",
-    })
-    const [code, stdout, stderr] = await Promise.all([
-      child.exited,
-      Bun.readableStreamToText(child.stdout),
-      Bun.readableStreamToText(child.stderr),
-    ])
-
-    expect(code, stderr).toBe(0)
-    const posts = JSON.parse(stdout) as Array<{ method: string; session: string | null }>
-    expect(posts.filter((post) => post.method === "initialize").map((post) => post.session)).toEqual([null, null])
-    expect(posts.filter((post) => post.method === "ping" && post.session === "expired")).toHaveLength(2)
-    expect(posts.filter((post) => post.method === "ping" && post.session === "replacement")).toHaveLength(2)
-  })
 })

+ 9 - 8
packages/opencode/test/tool/code-mode-integration.test.ts

@@ -8,14 +8,15 @@ import { Session } from "@/session/session"
 import { Tool } from "@/tool/tool"
 import * as Truncate from "@/tool/truncate"
 import { MessageID, SessionID } from "@/session/schema"
-import { Server } from "@modelcontextprotocol/server"
+import { Server } from "@modelcontextprotocol/sdk/server/index.js"
+import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
+import type { Client } from "@modelcontextprotocol/sdk/client/index.js"
 import {
-  InMemoryTransport,
+  CallToolRequestSchema,
   LATEST_PROTOCOL_VERSION,
-  type CallToolResult,
-  type Client,
+  ListToolsRequestSchema,
   type Tool as MCPToolDef,
-} from "@modelcontextprotocol/client"
+} from "@modelcontextprotocol/sdk/types.js"
 import { Cause, Effect, Exit, Layer } from "effect"
 
 const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
@@ -99,7 +100,7 @@ const TOOL_DEFS: MCPToolDef[] = [
   },
 ] as MCPToolDef[]
 
-function handleCall(name: string, args: Record<string, unknown>): CallToolResult {
+function handleCall(name: string, args: Record<string, unknown>) {
   switch (name) {
     case "get_text":
       return { content: [{ type: "text", text: `hello ${args.name}` }] }
@@ -121,8 +122,8 @@ let description: string
 
 async function buildTool() {
   const server = new Server({ name: SERVER, version: "1.0.0" }, { capabilities: { tools: {} } })
-  server.setRequestHandler("tools/list", async () => ({ tools: TOOL_DEFS }))
-  server.setRequestHandler("tools/call", async (req) =>
+  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS }))
+  server.setRequestHandler(CallToolRequestSchema, async (req) =>
     handleCall(req.params.name, (req.params.arguments ?? {}) as Record<string, unknown>),
   )
 

+ 1 - 1
packages/opencode/test/tool/code-mode.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect, test } from "bun:test"
 import { CODE_MODE_TOOL, CodeModeTool, Parameters, describeCatalog } from "@/tool/code-mode"
-import type { Tool as MCPToolDef } from "@modelcontextprotocol/client"
+import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js"
 import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Agent } from "@/agent/agent"
 import { MCP } from "@/mcp"

+ 1 - 1
packages/opencode/test/tool/registry.test.ts

@@ -20,7 +20,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { MCP } from "@/mcp"
-import type { Tool as MCPToolDef } from "@modelcontextprotocol/client"
+import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js"
 
 const configLayer = TestConfig.layer({
   directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".opencode")])),

+ 0 - 2
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -2328,7 +2328,6 @@ export class Auth2 extends HeyApiClient {
       directory?: string
       workspace?: string
       code?: string
-      iss?: string
     },
     options?: Options<never, ThrowOnError>,
   ) {
@@ -2341,7 +2340,6 @@ export class Auth2 extends HeyApiClient {
             { in: "query", key: "directory" },
             { in: "query", key: "workspace" },
             { in: "body", key: "code" },
-            { in: "body", key: "iss" },
           ],
         },
       ],

+ 0 - 1
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -8563,7 +8563,6 @@ export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartRespo
 export type McpAuthCallbackData = {
   body?: {
     code: string
-    iss?: string
   }
   path: {
     name: string

+ 0 - 214
patches/@modelcontextprotocol%2Fclient@2.0.0.patch

@@ -1,214 +0,0 @@
-diff --git a/dist/index.cjs b/dist/index.cjs
-index 635f1c0..9214f0b 100644
---- a/dist/index.cjs
-+++ b/dist/index.cjs
-@@ -3211,6 +3211,7 @@ var Client = class extends require_src.Protocol {
- 	*/
- 	async _connectPlainLegacy(transport, options) {
- 		await super.connect(transport);
-+		transport.onsessionexpired = () => this._legacyHandshake(transport, options);
- 		if (transport.sessionId !== void 0) {
- 			const negotiatedProtocolVersion = this._negotiatedProtocolVersion;
- 			if (negotiatedProtocolVersion !== void 0) transport.setProtocolVersion?.(negotiatedProtocolVersion);
-@@ -3227,6 +3228,7 @@ var Client = class extends require_src.Protocol {
- 	* the handshake; its completion sets the negotiated (legacy) version.
- 	*/
- 	async _legacyHandshake(transport, options) {
-+		transport.onsessionexpired = () => this._legacyHandshake(transport, options);
- 		const legacyVersions = require_src.legacyProtocolVersions(this._supportedProtocolVersions);
- 		try {
- 			const offeredVersion = legacyVersions[0];
-@@ -3265,6 +3267,7 @@ var Client = class extends require_src.Protocol {
- 			await super.connect(transport);
- 			const negotiatedProtocolVersion = this._negotiatedProtocolVersion;
- 			if (negotiatedProtocolVersion !== void 0 && transport.setProtocolVersion) transport.setProtocolVersion(negotiatedProtocolVersion);
-+			if (negotiatedProtocolVersion !== void 0 && !require_src.isModernProtocolVersion(negotiatedProtocolVersion)) transport.onsessionexpired = () => this._legacyHandshake(transport, options);
- 			return;
- 		}
- 		this._resetConnectionState();
-@@ -5294,10 +5297,32 @@ var StreamableHTTPClientTransport = class {
- 		}
- 	}
- 	async send(message, options) {
--		return this._send(message, options, false);
-+		return this._send(message, options, false, 0, false);
-+	}
-+	async _recoverSession(expiredSessionId) {
-+		if (this._sessionRecovery) return this._sessionRecovery;
-+		if (!this.onsessionexpired) return false;
-+		if (this._sessionId !== expiredSessionId) return true;
-+		this._sessionId = void 0;
-+		this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired()).then(() => true);
-+		try {
-+			return await this._sessionRecovery;
-+		} catch (error) {
-+			this._sessionId = void 0;
-+			await this.close();
-+			throw error;
-+		} finally {
-+			this._sessionRecovery = void 0;
-+		}
- 	}
--	async _send(message, options, isAuthRetry, stepUpRetries = 0) {
-+	async _send(message, options, isAuthRetry, stepUpRetries = 0, isSessionRetry = false) {
- 		try {
-+			const isHandshake = Array.isArray(message) ? message.some((m) => require_src.isInitializeRequest(m)) : require_src.isInitializeRequest(message);
-+			const isInitialized = Array.isArray(message) ? message.some((m) => require_src.isInitializedNotification(m)) : require_src.isInitializedNotification(message);
-+			if (this._sessionRecovery && !isHandshake && !isInitialized) {
-+				await this._sessionRecovery;
-+				options?.requestSignal?.throwIfAborted();
-+			}
- 			const { resumptionToken, onresumptiontoken } = options || {};
- 			if (resumptionToken) {
- 				this._startOrAuthSse({
-@@ -5309,8 +5334,8 @@ var StreamableHTTPClientTransport = class {
- 			}
- 			const headers = await this._commonHeaders();
- 			this._applyBodyDerivedHeaders(headers, message);
--			const isHandshake = Array.isArray(message) ? message.some((m) => require_src.isInitializeRequest(m)) : require_src.isInitializeRequest(message);
- 			if (isHandshake) headers.delete("mcp-session-id");
-+			const requestSessionId = headers.get("mcp-session-id") || void 0;
- 			if (options?.headers !== void 0) for (const [name, value] of Object.entries(options.headers)) {
- 				if (RESERVED_REQUEST_HEADER_NAMES.has(name.toLowerCase())) continue;
- 				headers.set(name, value);
-@@ -5332,8 +5357,14 @@ var StreamableHTTPClientTransport = class {
- 				signal
- 			};
- 			const response = await (this._fetch ?? fetch)(this._url, init);
--			if (isHandshake && response.ok) this._sessionId = response.headers.get("mcp-session-id") || void 0;
-+			if (isHandshake && response.ok && (requestSessionId === void 0 || this._sessionId === requestSessionId)) this._sessionId = response.headers.get("mcp-session-id") || void 0;
- 			if (!response.ok) {
-+				if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitialized) {
-+					if (await this._recoverSession(requestSessionId)) {
-+						options?.requestSignal?.throwIfAborted();
-+						return this._send(message, options, isAuthRetry, stepUpRetries, true);
-+					}
-+				}
- 				if (response.status === 401 && this._authProvider) {
- 					if (response.headers.has("www-authenticate")) {
- 						const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
-@@ -5351,7 +5382,7 @@ var StreamableHTTPClientTransport = class {
- 							throw markAuthSeamEscape(error);
- 						}
- 						await response.text?.().catch(() => {});
--						return this._send(message, options, true, stepUpRetries);
-+						return this._send(message, options, true, stepUpRetries, isSessionRetry);
- 					}
- 					await response.text?.().catch(() => {});
- 					if (isAuthRetry) throw markAuthSeamEscape(new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", {
-@@ -5371,7 +5402,7 @@ var StreamableHTTPClientTransport = class {
- 							statusText: response.statusText,
- 							text
- 						}, stepUpRetries) !== "AUTHORIZED") throw markAuthSeamEscape(new UnauthorizedError());
--						return this._send(message, options, isAuthRetry, stepUpRetries + 1);
-+						return this._send(message, options, isAuthRetry, stepUpRetries + 1, isSessionRetry);
- 					}
- 				}
- 				if (response.status === 400 && typeof text === "string" && this._isModernEnvelopedRequest(message)) try {
-diff --git a/dist/index.mjs b/dist/index.mjs
-index f02ce3c..0a5a649 100644
---- a/dist/index.mjs
-+++ b/dist/index.mjs
-@@ -3208,6 +3208,7 @@ var Client = class extends Protocol {
- 	*/
- 	async _connectPlainLegacy(transport, options) {
- 		await super.connect(transport);
-+		transport.onsessionexpired = () => this._legacyHandshake(transport, options);
- 		if (transport.sessionId !== void 0) {
- 			const negotiatedProtocolVersion = this._negotiatedProtocolVersion;
- 			if (negotiatedProtocolVersion !== void 0) transport.setProtocolVersion?.(negotiatedProtocolVersion);
-@@ -3224,6 +3225,7 @@ var Client = class extends Protocol {
- 	* the handshake; its completion sets the negotiated (legacy) version.
- 	*/
- 	async _legacyHandshake(transport, options) {
-+		transport.onsessionexpired = () => this._legacyHandshake(transport, options);
- 		const legacyVersions = legacyProtocolVersions(this._supportedProtocolVersions);
- 		try {
- 			const offeredVersion = legacyVersions[0];
-@@ -3262,6 +3264,7 @@ var Client = class extends Protocol {
- 			await super.connect(transport);
- 			const negotiatedProtocolVersion = this._negotiatedProtocolVersion;
- 			if (negotiatedProtocolVersion !== void 0 && transport.setProtocolVersion) transport.setProtocolVersion(negotiatedProtocolVersion);
-+			if (negotiatedProtocolVersion !== void 0 && !isModernProtocolVersion(negotiatedProtocolVersion)) transport.onsessionexpired = () => this._legacyHandshake(transport, options);
- 			return;
- 		}
- 		this._resetConnectionState();
-@@ -5291,10 +5294,32 @@ var StreamableHTTPClientTransport = class {
- 		}
- 	}
- 	async send(message, options) {
--		return this._send(message, options, false);
-+		return this._send(message, options, false, 0, false);
-+	}
-+	async _recoverSession(expiredSessionId) {
-+		if (this._sessionRecovery) return this._sessionRecovery;
-+		if (!this.onsessionexpired) return false;
-+		if (this._sessionId !== expiredSessionId) return true;
-+		this._sessionId = void 0;
-+		this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired()).then(() => true);
-+		try {
-+			return await this._sessionRecovery;
-+		} catch (error) {
-+			this._sessionId = void 0;
-+			await this.close();
-+			throw error;
-+		} finally {
-+			this._sessionRecovery = void 0;
-+		}
- 	}
--	async _send(message, options, isAuthRetry, stepUpRetries = 0) {
-+	async _send(message, options, isAuthRetry, stepUpRetries = 0, isSessionRetry = false) {
- 		try {
-+			const isHandshake = Array.isArray(message) ? message.some((m) => isInitializeRequest(m)) : isInitializeRequest(message);
-+			const isInitialized = Array.isArray(message) ? message.some((m) => isInitializedNotification(m)) : isInitializedNotification(message);
-+			if (this._sessionRecovery && !isHandshake && !isInitialized) {
-+				await this._sessionRecovery;
-+				options?.requestSignal?.throwIfAborted();
-+			}
- 			const { resumptionToken, onresumptiontoken } = options || {};
- 			if (resumptionToken) {
- 				this._startOrAuthSse({
-@@ -5306,8 +5331,8 @@ var StreamableHTTPClientTransport = class {
- 			}
- 			const headers = await this._commonHeaders();
- 			this._applyBodyDerivedHeaders(headers, message);
--			const isHandshake = Array.isArray(message) ? message.some((m) => isInitializeRequest(m)) : isInitializeRequest(message);
- 			if (isHandshake) headers.delete("mcp-session-id");
-+			const requestSessionId = headers.get("mcp-session-id") || void 0;
- 			if (options?.headers !== void 0) for (const [name, value] of Object.entries(options.headers)) {
- 				if (RESERVED_REQUEST_HEADER_NAMES.has(name.toLowerCase())) continue;
- 				headers.set(name, value);
-@@ -5329,8 +5354,14 @@ var StreamableHTTPClientTransport = class {
- 				signal
- 			};
- 			const response = await (this._fetch ?? fetch)(this._url, init);
--			if (isHandshake && response.ok) this._sessionId = response.headers.get("mcp-session-id") || void 0;
-+			if (isHandshake && response.ok && (requestSessionId === void 0 || this._sessionId === requestSessionId)) this._sessionId = response.headers.get("mcp-session-id") || void 0;
- 			if (!response.ok) {
-+				if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitialized) {
-+					if (await this._recoverSession(requestSessionId)) {
-+						options?.requestSignal?.throwIfAborted();
-+						return this._send(message, options, isAuthRetry, stepUpRetries, true);
-+					}
-+				}
- 				if (response.status === 401 && this._authProvider) {
- 					if (response.headers.has("www-authenticate")) {
- 						const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
-@@ -5348,7 +5379,7 @@ var StreamableHTTPClientTransport = class {
- 							throw markAuthSeamEscape(error);
- 						}
- 						await response.text?.().catch(() => {});
--						return this._send(message, options, true, stepUpRetries);
-+						return this._send(message, options, true, stepUpRetries, isSessionRetry);
- 					}
- 					await response.text?.().catch(() => {});
- 					if (isAuthRetry) throw markAuthSeamEscape(new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", {
-@@ -5368,7 +5399,7 @@ var StreamableHTTPClientTransport = class {
- 							statusText: response.statusText,
- 							text
- 						}, stepUpRetries) !== "AUTHORIZED") throw markAuthSeamEscape(new UnauthorizedError());
--						return this._send(message, options, isAuthRetry, stepUpRetries + 1);
-+						return this._send(message, options, isAuthRetry, stepUpRetries + 1, isSessionRetry);
- 					}
- 				}
- 				if (response.status === 400 && typeof text === "string" && this._isModernEnvelopedRequest(message)) try {

+ 629 - 0
patches/@modelcontextprotocol%2Fsdk@1.29.0.patch

@@ -0,0 +1,629 @@
+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<RequestT extends Request = Request, NotificationT ex
+      *
+      * For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
+      */
++    callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
++    callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
+     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<RequestT extends Request = Request, NotificationT ex
+      *
+      * For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
+      */
++    callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
++    callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
+     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.
+      */
+@@ -541,9 +547,11 @@ class Client extends protocol_js_1.Protocol {
+      * Called after listTools() to pre-compile validators for better performance.
+      */
+-    cacheToolMetadata(tools) {
+-        this._cachedToolOutputValidators.clear();
+-        this._cachedKnownTaskTools.clear();
+-        this._cachedRequiredTaskTools.clear();
++    cacheToolMetadata(tools, reset = true) {
++        if (reset) {
++            this._cachedToolOutputValidators.clear();
++            this._cachedKnownTaskTools.clear();
++            this._cachedRequiredTaskTools.clear();
++        }
+         for (const tool of tools) {
+             // If the tool has an outputSchema, create and cache the validator
+             if (tool.outputSchema) {
+@@ -569,7 +577,7 @@ class Client extends protocol_js_1.Protocol {
+     async listTools(params, options) {
+         const result = await this.request({ method: 'tools/list', params }, types_js_1.ListToolsResultSchema, options);
+         // Cache the tools and their output schemas for future validation
+-        this.cacheToolMetadata(result.tools);
++        this.cacheToolMetadata(result.tools, params?.cursor === undefined);
+         return result;
+     }
+     /**
+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/cjs/client/auth.d.ts b/dist/cjs/client/auth.d.ts
+index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
+--- a/dist/cjs/client/auth.d.ts
++++ b/dist/cjs/client/auth.d.ts
+@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
+  * @returns A Promise that resolves to an OAuthError instance
+  */
+ export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
++/**
++ * Selects scopes per the MCP spec and augments them for refresh token support.
++ */
++export declare function determineScope(options: {
++    requestedScope?: string;
++    resourceMetadata?: OAuthProtectedResourceMetadata;
++    authServerMetadata?: AuthorizationServerMetadata;
++    clientMetadata: OAuthClientMetadata;
++}): string | undefined;
+ /**
+  * Orchestrates the full auth flow with a server.
+  *
+diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js
+index c2e4fa91d26f5336889f6afa416147db75fc4872..178d7cfd96412d53bc14bbc13a8f76c11f727ee7 100644
+--- a/dist/cjs/client/auth.js
++++ b/dist/cjs/client/auth.js
+@@ -7,6 +7,7 @@ exports.UnauthorizedError = void 0;
+ exports.selectClientAuthMethod = selectClientAuthMethod;
+ exports.parseErrorResponse = parseErrorResponse;
+ exports.auth = auth;
++exports.determineScope = determineScope;
+ exports.isHttpsUrl = isHttpsUrl;
+ exports.selectResourceURL = selectResourceURL;
+ exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams;
+@@ -186,6 +187,19 @@ async function auth(provider, options) {
+         throw error;
+     }
+ }
++/**
++ * Selects scopes per the MCP spec and augments them for refresh token support.
++ */
++function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
++    let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
++    if (effectiveScope &&
++        authServerMetadata?.scopes_supported?.includes('offline_access') &&
++        !effectiveScope.split(' ').includes('offline_access') &&
++        clientMetadata.grant_types?.includes('refresh_token')) {
++        effectiveScope = `${effectiveScope} offline_access`;
++    }
++    return effectiveScope;
++}
+ async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
+     // Check if the provider has cached discovery state to skip discovery
+     const cachedState = await provider.discoveryState?.();
+@@ -241,12 +255,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
+         });
+     }
+     const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
+-    // Apply scope selection strategy (SEP-835):
+-    // 1. WWW-Authenticate scope (passed via `scope` param)
+-    // 2. PRM scopes_supported
+-    // 3. Client metadata scope (user-configured fallback)
+-    // The resolved scope is used consistently for both DCR and the authorization request.
+-    const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
++    const resolvedScope = determineScope({
++        requestedScope: scope,
++        resourceMetadata,
++        authServerMetadata: metadata,
++        clientMetadata: provider.clientMetadata
++    });
+     // Handle client registration if needed
+     let clientInformation = await Promise.resolve(provider.clientInformation());
+     if (!clientInformation) {
+@@ -741,7 +755,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
+     if (scope) {
+         authorizationUrl.searchParams.set('scope', scope);
+     }
+-    if (scope?.includes('offline_access')) {
++    if (scope?.split(' ').includes('offline_access')) {
+         // if the request includes the OIDC-only "offline_access" scope,
+         // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
+         // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
+diff --git a/dist/esm/client/auth.d.ts b/dist/esm/client/auth.d.ts
+index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
+--- a/dist/esm/client/auth.d.ts
++++ b/dist/esm/client/auth.d.ts
+@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
+  * @returns A Promise that resolves to an OAuthError instance
+  */
+ export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
++/**
++ * Selects scopes per the MCP spec and augments them for refresh token support.
++ */
++export declare function determineScope(options: {
++    requestedScope?: string;
++    resourceMetadata?: OAuthProtectedResourceMetadata;
++    authServerMetadata?: AuthorizationServerMetadata;
++    clientMetadata: OAuthClientMetadata;
++}): string | undefined;
+ /**
+  * Orchestrates the full auth flow with a server.
+  *
+diff --git a/dist/esm/client/auth.js b/dist/esm/client/auth.js
+index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af61dcad98b 100644
+--- a/dist/esm/client/auth.js
++++ b/dist/esm/client/auth.js
+@@ -161,6 +161,19 @@ export async function auth(provider, options) {
+         throw error;
+     }
+ }
++/**
++ * Selects scopes per the MCP spec and augments them for refresh token support.
++ */
++export function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
++    let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
++    if (effectiveScope &&
++        authServerMetadata?.scopes_supported?.includes('offline_access') &&
++        !effectiveScope.split(' ').includes('offline_access') &&
++        clientMetadata.grant_types?.includes('refresh_token')) {
++        effectiveScope = `${effectiveScope} offline_access`;
++    }
++    return effectiveScope;
++}
+ async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
+     // Check if the provider has cached discovery state to skip discovery
+     const cachedState = await provider.discoveryState?.();
+@@ -216,12 +229,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
+         });
+     }
+     const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
+-    // Apply scope selection strategy (SEP-835):
+-    // 1. WWW-Authenticate scope (passed via `scope` param)
+-    // 2. PRM scopes_supported
+-    // 3. Client metadata scope (user-configured fallback)
+-    // The resolved scope is used consistently for both DCR and the authorization request.
+-    const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
++    const resolvedScope = determineScope({
++        requestedScope: scope,
++        resourceMetadata,
++        authServerMetadata: metadata,
++        clientMetadata: provider.clientMetadata
++    });
+     // Handle client registration if needed
+     let clientInformation = await Promise.resolve(provider.clientInformation());
+     if (!clientInformation) {
+@@ -716,7 +729,7 @@ export async function startAuthorization(authorizationServerUrl, { metadata, cli
+     if (scope) {
+         authorizationUrl.searchParams.set('scope', scope);
+     }
+-    if (scope?.includes('offline_access')) {
++    if (scope?.split(' ').includes('offline_access')) {
+         // if the request includes the OIDC-only "offline_access" scope,
+         // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
+         // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
+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.
+      */
+@@ -537,9 +543,11 @@ export class Client extends Protocol {
+      * Called after listTools() to pre-compile validators for better performance.
+      */
+-    cacheToolMetadata(tools) {
+-        this._cachedToolOutputValidators.clear();
+-        this._cachedKnownTaskTools.clear();
+-        this._cachedRequiredTaskTools.clear();
++    cacheToolMetadata(tools, reset = true) {
++        if (reset) {
++            this._cachedToolOutputValidators.clear();
++            this._cachedKnownTaskTools.clear();
++            this._cachedRequiredTaskTools.clear();
++        }
+         for (const tool of tools) {
+             // If the tool has an outputSchema, create and cache the validator
+             if (tool.outputSchema) {
+@@ -565,7 +573,7 @@ export class Client extends Protocol {
+     async listTools(params, options) {
+         const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options);
+         // Cache the tools and their output schemas for future validation
+-        this.cacheToolMetadata(result.tools);
++        this.cacheToolMetadata(result.tools, params?.cursor === undefined);
+         return result;
+     }
+     /**
+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);
+                 });