Преглед изворни кода

feat(core): add mcp support (#34513)

Aiden Cline пре 1 месец
родитељ
комит
b1ca070b3b

+ 65 - 76
bun.lock

@@ -304,6 +304,7 @@
         "@effect/sql-sqlite-bun": "catalog:",
         "@ff-labs/fff-bun": "0.9.4",
         "@lydell/node-pty": "catalog:",
+        "@modelcontextprotocol/sdk": "1.29.0",
         "@npmcli/arborist": "9.4.0",
         "@npmcli/config": "10.8.1",
         "@opencode-ai/effect-drizzle-sqlite": "workspace:*",
@@ -3014,7 +3015,7 @@
 
     "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
 
-    "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
+    "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
 
     "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
 
@@ -3184,7 +3185,7 @@
 
     "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
 
-    "body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="],
+    "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=="],
 
     "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="],
 
@@ -3352,7 +3353,7 @@
 
     "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
 
-    "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
+    "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
 
     "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
 
@@ -3362,7 +3363,7 @@
 
     "cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="],
 
-    "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
+    "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
 
     "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
 
@@ -3670,7 +3671,7 @@
 
     "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
 
-    "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": ["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=="],
 
     "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="],
 
@@ -3728,7 +3729,7 @@
 
     "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
 
-    "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
+    "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=="],
 
     "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
 
@@ -3766,7 +3767,7 @@
 
     "framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="],
 
-    "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
+    "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
 
     "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
 
@@ -4330,11 +4331,11 @@
 
     "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
 
-    "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
+    "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
 
     "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
 
-    "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
+    "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
 
     "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
 
@@ -4820,7 +4821,7 @@
 
     "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
 
-    "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
+    "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=="],
 
     "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
 
@@ -5006,7 +5007,7 @@
 
     "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
 
-    "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
+    "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=="],
 
     "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
 
@@ -5016,7 +5017,7 @@
 
     "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="],
 
-    "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
+    "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=="],
 
     "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
 
@@ -5332,7 +5333,7 @@
 
     "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
 
-    "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
+    "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=="],
 
     "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
 
@@ -5910,14 +5911,10 @@
 
     "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
 
-    "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
-
     "@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="],
 
     "@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
 
-    "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
-
     "@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
 
     "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
@@ -6050,8 +6047,12 @@
 
     "@shikijs/themes/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
 
+    "@slack/bolt/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=="],
+
     "@slack/bolt/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
 
+    "@slack/bolt/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
+
     "@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="],
 
     "@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="],
@@ -6118,10 +6119,6 @@
 
     "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
 
-    "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
-
-    "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
-
     "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="],
 
     "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
@@ -6176,10 +6173,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/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=="],
-
     "builder-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 
     "builder-util/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
@@ -6258,16 +6251,10 @@
 
     "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
 
-    "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
-
-    "express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
-
     "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
 
     "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
 
-    "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
-
     "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
 
     "fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
@@ -6390,8 +6377,6 @@
 
     "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
 
-    "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
-
     "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
 
     "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@@ -6400,10 +6385,6 @@
 
     "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=="],
-
     "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
 
     "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@@ -6448,7 +6429,7 @@
 
     "tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="],
 
-    "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+    "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
 
     "unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
 
@@ -6718,28 +6699,6 @@
 
     "@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=="],
@@ -6862,6 +6821,34 @@
 
     "@shikijs/stream/@shikijs/core/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
 
+    "@slack/bolt/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
+
+    "@slack/bolt/express/body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="],
+
+    "@slack/bolt/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
+
+    "@slack/bolt/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
+    "@slack/bolt/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
+
+    "@slack/bolt/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
+
+    "@slack/bolt/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
+
+    "@slack/bolt/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
+
+    "@slack/bolt/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
+
+    "@slack/bolt/express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
+
+    "@slack/bolt/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
+
+    "@slack/bolt/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
+
+    "@slack/bolt/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
+
+    "@slack/bolt/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
+
     "@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
 
     "@slack/web-api/p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
@@ -6892,8 +6879,6 @@
 
     "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
 
-    "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
-
     "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
 
     "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
@@ -6958,8 +6943,6 @@
 
     "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
 
-    "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
-
     "builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
 
     "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
@@ -7018,12 +7001,8 @@
 
     "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
 
-    "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
-
     "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
 
-    "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
-
     "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
 
     "iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
@@ -7060,8 +7039,6 @@
 
     "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
 
-    "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
-
     "storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
 
     "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -7076,8 +7053,6 @@
 
     "tw-to-css/tailwindcss/postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
 
-    "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
-
     "unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
 
     "unzipper/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
@@ -7266,10 +7241,6 @@
 
     "@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
 
-    "@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
-
-    "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
-
     "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
 
     "@octokit/auth-app/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
@@ -7302,6 +7273,20 @@
 
     "@sentry/bundler-plugin-core/glob/path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
 
+    "@slack/bolt/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+    "@slack/bolt/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
+
+    "@slack/bolt/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
+
+    "@slack/bolt/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
+
+    "@slack/bolt/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
+
+    "@slack/bolt/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
+
+    "@slack/bolt/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
     "@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
 
     "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
@@ -7430,6 +7415,10 @@
 
     "@sentry/bundler-plugin-core/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 
+    "@slack/bolt/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+    "@slack/bolt/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
     "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
 
     "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],

+ 229 - 221
packages/client/src/generated-effect/client.ts

@@ -410,83 +410,90 @@ const adaptGroup8 = (raw: RawClient["server.integration"]) => ({
   attemptCancel: Endpoint8_6(raw),
 })
 
-type Endpoint9_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
-type Endpoint9_0Input = {
-  readonly credentialID: Endpoint9_0Request["params"]["credentialID"]
-  readonly location?: Endpoint9_0Request["query"]["location"]
-  readonly label: Endpoint9_0Request["payload"]["label"]
+type Endpoint9_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
+type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] }
+const Endpoint9_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint9_0Input) =>
+  raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
+
+const adaptGroup9 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint9_0(raw) })
+
+type Endpoint10_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
+type Endpoint10_0Input = {
+  readonly credentialID: Endpoint10_0Request["params"]["credentialID"]
+  readonly location?: Endpoint10_0Request["query"]["location"]
+  readonly label: Endpoint10_0Request["payload"]["label"]
 }
-const Endpoint9_0 = (raw: RawClient["server.credential"]) => (input: Endpoint9_0Input) =>
+const Endpoint10_0 = (raw: RawClient["server.credential"]) => (input: Endpoint10_0Input) =>
   raw["credential.update"]({
     params: { credentialID: input["credentialID"] },
     query: { location: input["location"] },
     payload: { label: input["label"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint9_1Request = Parameters<RawClient["server.credential"]["credential.remove"]>[0]
-type Endpoint9_1Input = {
-  readonly credentialID: Endpoint9_1Request["params"]["credentialID"]
-  readonly location?: Endpoint9_1Request["query"]["location"]
+type Endpoint10_1Request = Parameters<RawClient["server.credential"]["credential.remove"]>[0]
+type Endpoint10_1Input = {
+  readonly credentialID: Endpoint10_1Request["params"]["credentialID"]
+  readonly location?: Endpoint10_1Request["query"]["location"]
 }
-const Endpoint9_1 = (raw: RawClient["server.credential"]) => (input: Endpoint9_1Input) =>
+const Endpoint10_1 = (raw: RawClient["server.credential"]) => (input: Endpoint10_1Input) =>
   raw["credential.remove"]({
     params: { credentialID: input["credentialID"] },
     query: { location: input["location"] },
   }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup9 = (raw: RawClient["server.credential"]) => ({ update: Endpoint9_0(raw), remove: Endpoint9_1(raw) })
+const adaptGroup10 = (raw: RawClient["server.credential"]) => ({ update: Endpoint10_0(raw), remove: Endpoint10_1(raw) })
 
-type Endpoint10_0Request = Parameters<RawClient["server.project"]["project.current"]>[0]
-type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] }
-const Endpoint10_0 = (raw: RawClient["server.project"]) => (input?: Endpoint10_0Input) =>
+type Endpoint11_0Request = Parameters<RawClient["server.project"]["project.current"]>[0]
+type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] }
+const Endpoint11_0 = (raw: RawClient["server.project"]) => (input?: Endpoint11_0Input) =>
   raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint10_1Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
-type Endpoint10_1Input = {
-  readonly projectID: Endpoint10_1Request["params"]["projectID"]
-  readonly location?: Endpoint10_1Request["query"]["location"]
+type Endpoint11_1Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
+type Endpoint11_1Input = {
+  readonly projectID: Endpoint11_1Request["params"]["projectID"]
+  readonly location?: Endpoint11_1Request["query"]["location"]
 }
-const Endpoint10_1 = (raw: RawClient["server.project"]) => (input: Endpoint10_1Input) =>
+const Endpoint11_1 = (raw: RawClient["server.project"]) => (input: Endpoint11_1Input) =>
   raw["project.directories"]({
     params: { projectID: input["projectID"] },
     query: { location: input["location"] },
   }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup10 = (raw: RawClient["server.project"]) => ({
-  current: Endpoint10_0(raw),
-  directories: Endpoint10_1(raw),
+const adaptGroup11 = (raw: RawClient["server.project"]) => ({
+  current: Endpoint11_0(raw),
+  directories: Endpoint11_1(raw),
 })
 
-type Endpoint11_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
-type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] }
-const Endpoint11_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint11_0Input) =>
+type Endpoint12_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
+type Endpoint12_0Input = { readonly location?: Endpoint12_0Request["query"]["location"] }
+const Endpoint12_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint12_0Input) =>
   raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint11_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
-type Endpoint11_1Input = { readonly projectID?: Endpoint11_1Request["query"]["projectID"] }
-const Endpoint11_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint11_1Input) =>
+type Endpoint12_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
+type Endpoint12_1Input = { readonly projectID?: Endpoint12_1Request["query"]["projectID"] }
+const Endpoint12_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint12_1Input) =>
   raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe(
     Effect.mapError(mapClientError),
     Effect.map((value) => value.data),
   )
 
-type Endpoint11_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
-type Endpoint11_2Input = { readonly id: Endpoint11_2Request["params"]["id"] }
-const Endpoint11_2 = (raw: RawClient["server.permission"]) => (input: Endpoint11_2Input) =>
+type Endpoint12_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
+type Endpoint12_2Input = { readonly id: Endpoint12_2Request["params"]["id"] }
+const Endpoint12_2 = (raw: RawClient["server.permission"]) => (input: Endpoint12_2Input) =>
   raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint11_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
-type Endpoint11_3Input = {
-  readonly sessionID: Endpoint11_3Request["params"]["sessionID"]
-  readonly id?: Endpoint11_3Request["payload"]["id"]
-  readonly action: Endpoint11_3Request["payload"]["action"]
-  readonly resources: Endpoint11_3Request["payload"]["resources"]
-  readonly save?: Endpoint11_3Request["payload"]["save"]
-  readonly metadata?: Endpoint11_3Request["payload"]["metadata"]
-  readonly source?: Endpoint11_3Request["payload"]["source"]
-  readonly agent?: Endpoint11_3Request["payload"]["agent"]
-}
-const Endpoint11_3 = (raw: RawClient["server.permission"]) => (input: Endpoint11_3Input) =>
+type Endpoint12_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
+type Endpoint12_3Input = {
+  readonly sessionID: Endpoint12_3Request["params"]["sessionID"]
+  readonly id?: Endpoint12_3Request["payload"]["id"]
+  readonly action: Endpoint12_3Request["payload"]["action"]
+  readonly resources: Endpoint12_3Request["payload"]["resources"]
+  readonly save?: Endpoint12_3Request["payload"]["save"]
+  readonly metadata?: Endpoint12_3Request["payload"]["metadata"]
+  readonly source?: Endpoint12_3Request["payload"]["source"]
+  readonly agent?: Endpoint12_3Request["payload"]["agent"]
+}
+const Endpoint12_3 = (raw: RawClient["server.permission"]) => (input: Endpoint12_3Input) =>
   raw["session.permission.create"]({
     params: { sessionID: input["sessionID"] },
     payload: {
@@ -503,87 +510,87 @@ const Endpoint11_3 = (raw: RawClient["server.permission"]) => (input: Endpoint11
     Effect.map((value) => value.data),
   )
 
-type Endpoint11_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
-type Endpoint11_4Input = { readonly sessionID: Endpoint11_4Request["params"]["sessionID"] }
-const Endpoint11_4 = (raw: RawClient["server.permission"]) => (input: Endpoint11_4Input) =>
+type Endpoint12_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
+type Endpoint12_4Input = { readonly sessionID: Endpoint12_4Request["params"]["sessionID"] }
+const Endpoint12_4 = (raw: RawClient["server.permission"]) => (input: Endpoint12_4Input) =>
   raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
     Effect.mapError(mapClientError),
     Effect.map((value) => value.data),
   )
 
-type Endpoint11_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
-type Endpoint11_5Input = {
-  readonly sessionID: Endpoint11_5Request["params"]["sessionID"]
-  readonly requestID: Endpoint11_5Request["params"]["requestID"]
+type Endpoint12_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
+type Endpoint12_5Input = {
+  readonly sessionID: Endpoint12_5Request["params"]["sessionID"]
+  readonly requestID: Endpoint12_5Request["params"]["requestID"]
 }
-const Endpoint11_5 = (raw: RawClient["server.permission"]) => (input: Endpoint11_5Input) =>
+const Endpoint12_5 = (raw: RawClient["server.permission"]) => (input: Endpoint12_5Input) =>
   raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
     Effect.mapError(mapClientError),
     Effect.map((value) => value.data),
   )
 
-type Endpoint11_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
-type Endpoint11_6Input = {
-  readonly sessionID: Endpoint11_6Request["params"]["sessionID"]
-  readonly requestID: Endpoint11_6Request["params"]["requestID"]
-  readonly reply: Endpoint11_6Request["payload"]["reply"]
-  readonly message?: Endpoint11_6Request["payload"]["message"]
+type Endpoint12_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
+type Endpoint12_6Input = {
+  readonly sessionID: Endpoint12_6Request["params"]["sessionID"]
+  readonly requestID: Endpoint12_6Request["params"]["requestID"]
+  readonly reply: Endpoint12_6Request["payload"]["reply"]
+  readonly message?: Endpoint12_6Request["payload"]["message"]
 }
-const Endpoint11_6 = (raw: RawClient["server.permission"]) => (input: Endpoint11_6Input) =>
+const Endpoint12_6 = (raw: RawClient["server.permission"]) => (input: Endpoint12_6Input) =>
   raw["session.permission.reply"]({
     params: { sessionID: input["sessionID"], requestID: input["requestID"] },
     payload: { reply: input["reply"], message: input["message"] },
   }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup11 = (raw: RawClient["server.permission"]) => ({
-  listRequests: Endpoint11_0(raw),
-  listSaved: Endpoint11_1(raw),
-  removeSaved: Endpoint11_2(raw),
-  create: Endpoint11_3(raw),
-  list: Endpoint11_4(raw),
-  get: Endpoint11_5(raw),
-  reply: Endpoint11_6(raw),
+const adaptGroup12 = (raw: RawClient["server.permission"]) => ({
+  listRequests: Endpoint12_0(raw),
+  listSaved: Endpoint12_1(raw),
+  removeSaved: Endpoint12_2(raw),
+  create: Endpoint12_3(raw),
+  list: Endpoint12_4(raw),
+  get: Endpoint12_5(raw),
+  reply: Endpoint12_6(raw),
 })
 
-type Endpoint12_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
-type Endpoint12_0Input = {
-  readonly location?: Endpoint12_0Request["query"]["location"]
-  readonly path?: Endpoint12_0Request["query"]["path"]
+type Endpoint13_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
+type Endpoint13_0Input = {
+  readonly location?: Endpoint13_0Request["query"]["location"]
+  readonly path?: Endpoint13_0Request["query"]["path"]
 }
-const Endpoint12_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint12_0Input) =>
+const Endpoint13_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint13_0Input) =>
   raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-type Endpoint12_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
-type Endpoint12_1Input = {
-  readonly location?: Endpoint12_1Request["query"]["location"]
-  readonly query: Endpoint12_1Request["query"]["query"]
-  readonly type?: Endpoint12_1Request["query"]["type"]
-  readonly limit?: Endpoint12_1Request["query"]["limit"]
+type Endpoint13_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
+type Endpoint13_1Input = {
+  readonly location?: Endpoint13_1Request["query"]["location"]
+  readonly query: Endpoint13_1Request["query"]["query"]
+  readonly type?: Endpoint13_1Request["query"]["type"]
+  readonly limit?: Endpoint13_1Request["query"]["limit"]
 }
-const Endpoint12_1 = (raw: RawClient["server.fs"]) => (input: Endpoint12_1Input) =>
+const Endpoint13_1 = (raw: RawClient["server.fs"]) => (input: Endpoint13_1Input) =>
   raw["fs.find"]({
     query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
   }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup12 = (raw: RawClient["server.fs"]) => ({ list: Endpoint12_0(raw), find: Endpoint12_1(raw) })
+const adaptGroup13 = (raw: RawClient["server.fs"]) => ({ list: Endpoint13_0(raw), find: Endpoint13_1(raw) })
 
-type Endpoint13_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
-type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
-const Endpoint13_0 = (raw: RawClient["server.command"]) => (input?: Endpoint13_0Input) =>
+type Endpoint14_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
+type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
+const Endpoint14_0 = (raw: RawClient["server.command"]) => (input?: Endpoint14_0Input) =>
   raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup13 = (raw: RawClient["server.command"]) => ({ list: Endpoint13_0(raw) })
+const adaptGroup14 = (raw: RawClient["server.command"]) => ({ list: Endpoint14_0(raw) })
 
-type Endpoint14_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
-type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
-const Endpoint14_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint14_0Input) =>
+type Endpoint15_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
+type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] }
+const Endpoint15_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint15_0Input) =>
   raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup14 = (raw: RawClient["server.skill"]) => ({ list: Endpoint14_0(raw) })
+const adaptGroup15 = (raw: RawClient["server.skill"]) => ({ list: Endpoint15_0(raw) })
 
-const Endpoint15_0 = (raw: RawClient["server.event"]) => () =>
+const Endpoint16_0 = (raw: RawClient["server.event"]) => () =>
   Stream.unwrap(
     raw["event.subscribe"]({}).pipe(
       Effect.mapError(mapClientError),
@@ -591,23 +598,23 @@ const Endpoint15_0 = (raw: RawClient["server.event"]) => () =>
     ),
   )
 
-const adaptGroup15 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint15_0(raw) })
+const adaptGroup16 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint16_0(raw) })
 
-type Endpoint16_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
-type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
-const Endpoint16_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint16_0Input) =>
+type Endpoint17_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
+type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] }
+const Endpoint17_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint17_0Input) =>
   raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint16_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
-type Endpoint16_1Input = {
-  readonly location?: Endpoint16_1Request["query"]["location"]
-  readonly command?: Endpoint16_1Request["payload"]["command"]
-  readonly args?: Endpoint16_1Request["payload"]["args"]
-  readonly cwd?: Endpoint16_1Request["payload"]["cwd"]
-  readonly title?: Endpoint16_1Request["payload"]["title"]
-  readonly env?: Endpoint16_1Request["payload"]["env"]
+type Endpoint17_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
+type Endpoint17_1Input = {
+  readonly location?: Endpoint17_1Request["query"]["location"]
+  readonly command?: Endpoint17_1Request["payload"]["command"]
+  readonly args?: Endpoint17_1Request["payload"]["args"]
+  readonly cwd?: Endpoint17_1Request["payload"]["cwd"]
+  readonly title?: Endpoint17_1Request["payload"]["title"]
+  readonly env?: Endpoint17_1Request["payload"]["env"]
 }
-const Endpoint16_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint16_1Input) =>
+const Endpoint17_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint17_1Input) =>
   raw["pty.create"]({
     query: { location: input?.["location"] },
     payload: {
@@ -619,201 +626,201 @@ const Endpoint16_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint16_1Inpu
     },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint16_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
-type Endpoint16_2Input = {
-  readonly ptyID: Endpoint16_2Request["params"]["ptyID"]
-  readonly location?: Endpoint16_2Request["query"]["location"]
+type Endpoint17_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
+type Endpoint17_2Input = {
+  readonly ptyID: Endpoint17_2Request["params"]["ptyID"]
+  readonly location?: Endpoint17_2Request["query"]["location"]
 }
-const Endpoint16_2 = (raw: RawClient["server.pty"]) => (input: Endpoint16_2Input) =>
+const Endpoint17_2 = (raw: RawClient["server.pty"]) => (input: Endpoint17_2Input) =>
   raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-type Endpoint16_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
-type Endpoint16_3Input = {
-  readonly ptyID: Endpoint16_3Request["params"]["ptyID"]
-  readonly location?: Endpoint16_3Request["query"]["location"]
-  readonly title?: Endpoint16_3Request["payload"]["title"]
-  readonly size?: Endpoint16_3Request["payload"]["size"]
+type Endpoint17_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
+type Endpoint17_3Input = {
+  readonly ptyID: Endpoint17_3Request["params"]["ptyID"]
+  readonly location?: Endpoint17_3Request["query"]["location"]
+  readonly title?: Endpoint17_3Request["payload"]["title"]
+  readonly size?: Endpoint17_3Request["payload"]["size"]
 }
-const Endpoint16_3 = (raw: RawClient["server.pty"]) => (input: Endpoint16_3Input) =>
+const Endpoint17_3 = (raw: RawClient["server.pty"]) => (input: Endpoint17_3Input) =>
   raw["pty.update"]({
     params: { ptyID: input["ptyID"] },
     query: { location: input["location"] },
     payload: { title: input["title"], size: input["size"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint16_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
-type Endpoint16_4Input = {
-  readonly ptyID: Endpoint16_4Request["params"]["ptyID"]
-  readonly location?: Endpoint16_4Request["query"]["location"]
+type Endpoint17_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
+type Endpoint17_4Input = {
+  readonly ptyID: Endpoint17_4Request["params"]["ptyID"]
+  readonly location?: Endpoint17_4Request["query"]["location"]
 }
-const Endpoint16_4 = (raw: RawClient["server.pty"]) => (input: Endpoint16_4Input) =>
+const Endpoint17_4 = (raw: RawClient["server.pty"]) => (input: Endpoint17_4Input) =>
   raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-const adaptGroup16 = (raw: RawClient["server.pty"]) => ({
-  list: Endpoint16_0(raw),
-  create: Endpoint16_1(raw),
-  get: Endpoint16_2(raw),
-  update: Endpoint16_3(raw),
-  remove: Endpoint16_4(raw),
+const adaptGroup17 = (raw: RawClient["server.pty"]) => ({
+  list: Endpoint17_0(raw),
+  create: Endpoint17_1(raw),
+  get: Endpoint17_2(raw),
+  update: Endpoint17_3(raw),
+  remove: Endpoint17_4(raw),
 })
 
-type Endpoint17_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
-type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] }
-const Endpoint17_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint17_0Input) =>
+type Endpoint18_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
+type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] }
+const Endpoint18_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint18_0Input) =>
   raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint17_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
-type Endpoint17_1Input = {
-  readonly location?: Endpoint17_1Request["query"]["location"]
-  readonly command: Endpoint17_1Request["payload"]["command"]
-  readonly cwd?: Endpoint17_1Request["payload"]["cwd"]
-  readonly timeout?: Endpoint17_1Request["payload"]["timeout"]
-  readonly metadata?: Endpoint17_1Request["payload"]["metadata"]
+type Endpoint18_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
+type Endpoint18_1Input = {
+  readonly location?: Endpoint18_1Request["query"]["location"]
+  readonly command: Endpoint18_1Request["payload"]["command"]
+  readonly cwd?: Endpoint18_1Request["payload"]["cwd"]
+  readonly timeout?: Endpoint18_1Request["payload"]["timeout"]
+  readonly metadata?: Endpoint18_1Request["payload"]["metadata"]
 }
-const Endpoint17_1 = (raw: RawClient["server.shell"]) => (input: Endpoint17_1Input) =>
+const Endpoint18_1 = (raw: RawClient["server.shell"]) => (input: Endpoint18_1Input) =>
   raw["shell.create"]({
     query: { location: input["location"] },
     payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint17_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
-type Endpoint17_2Input = {
-  readonly id: Endpoint17_2Request["params"]["id"]
-  readonly location?: Endpoint17_2Request["query"]["location"]
+type Endpoint18_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
+type Endpoint18_2Input = {
+  readonly id: Endpoint18_2Request["params"]["id"]
+  readonly location?: Endpoint18_2Request["query"]["location"]
 }
-const Endpoint17_2 = (raw: RawClient["server.shell"]) => (input: Endpoint17_2Input) =>
+const Endpoint18_2 = (raw: RawClient["server.shell"]) => (input: Endpoint18_2Input) =>
   raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-type Endpoint17_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
-type Endpoint17_3Input = {
-  readonly id: Endpoint17_3Request["params"]["id"]
-  readonly location?: Endpoint17_3Request["query"]["location"]
-  readonly cursor?: Endpoint17_3Request["query"]["cursor"]
-  readonly limit?: Endpoint17_3Request["query"]["limit"]
+type Endpoint18_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
+type Endpoint18_3Input = {
+  readonly id: Endpoint18_3Request["params"]["id"]
+  readonly location?: Endpoint18_3Request["query"]["location"]
+  readonly cursor?: Endpoint18_3Request["query"]["cursor"]
+  readonly limit?: Endpoint18_3Request["query"]["limit"]
 }
-const Endpoint17_3 = (raw: RawClient["server.shell"]) => (input: Endpoint17_3Input) =>
+const Endpoint18_3 = (raw: RawClient["server.shell"]) => (input: Endpoint18_3Input) =>
   raw["shell.output"]({
     params: { id: input["id"] },
     query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint17_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
-type Endpoint17_4Input = {
-  readonly id: Endpoint17_4Request["params"]["id"]
-  readonly location?: Endpoint17_4Request["query"]["location"]
+type Endpoint18_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
+type Endpoint18_4Input = {
+  readonly id: Endpoint18_4Request["params"]["id"]
+  readonly location?: Endpoint18_4Request["query"]["location"]
 }
-const Endpoint17_4 = (raw: RawClient["server.shell"]) => (input: Endpoint17_4Input) =>
+const Endpoint18_4 = (raw: RawClient["server.shell"]) => (input: Endpoint18_4Input) =>
   raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-const adaptGroup17 = (raw: RawClient["server.shell"]) => ({
-  list: Endpoint17_0(raw),
-  create: Endpoint17_1(raw),
-  get: Endpoint17_2(raw),
-  output: Endpoint17_3(raw),
-  remove: Endpoint17_4(raw),
+const adaptGroup18 = (raw: RawClient["server.shell"]) => ({
+  list: Endpoint18_0(raw),
+  create: Endpoint18_1(raw),
+  get: Endpoint18_2(raw),
+  output: Endpoint18_3(raw),
+  remove: Endpoint18_4(raw),
 })
 
-type Endpoint18_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
-type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] }
-const Endpoint18_0 = (raw: RawClient["server.question"]) => (input?: Endpoint18_0Input) =>
+type Endpoint19_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
+type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
+const Endpoint19_0 = (raw: RawClient["server.question"]) => (input?: Endpoint19_0Input) =>
   raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint18_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
-type Endpoint18_1Input = { readonly sessionID: Endpoint18_1Request["params"]["sessionID"] }
-const Endpoint18_1 = (raw: RawClient["server.question"]) => (input: Endpoint18_1Input) =>
+type Endpoint19_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
+type Endpoint19_1Input = { readonly sessionID: Endpoint19_1Request["params"]["sessionID"] }
+const Endpoint19_1 = (raw: RawClient["server.question"]) => (input: Endpoint19_1Input) =>
   raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
     Effect.mapError(mapClientError),
     Effect.map((value) => value.data),
   )
 
-type Endpoint18_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
-type Endpoint18_2Input = {
-  readonly sessionID: Endpoint18_2Request["params"]["sessionID"]
-  readonly requestID: Endpoint18_2Request["params"]["requestID"]
-  readonly answers: Endpoint18_2Request["payload"]["answers"]
+type Endpoint19_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
+type Endpoint19_2Input = {
+  readonly sessionID: Endpoint19_2Request["params"]["sessionID"]
+  readonly requestID: Endpoint19_2Request["params"]["requestID"]
+  readonly answers: Endpoint19_2Request["payload"]["answers"]
 }
-const Endpoint18_2 = (raw: RawClient["server.question"]) => (input: Endpoint18_2Input) =>
+const Endpoint19_2 = (raw: RawClient["server.question"]) => (input: Endpoint19_2Input) =>
   raw["session.question.reply"]({
     params: { sessionID: input["sessionID"], requestID: input["requestID"] },
     payload: { answers: input["answers"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint18_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
-type Endpoint18_3Input = {
-  readonly sessionID: Endpoint18_3Request["params"]["sessionID"]
-  readonly requestID: Endpoint18_3Request["params"]["requestID"]
+type Endpoint19_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
+type Endpoint19_3Input = {
+  readonly sessionID: Endpoint19_3Request["params"]["sessionID"]
+  readonly requestID: Endpoint19_3Request["params"]["requestID"]
 }
-const Endpoint18_3 = (raw: RawClient["server.question"]) => (input: Endpoint18_3Input) =>
+const Endpoint19_3 = (raw: RawClient["server.question"]) => (input: Endpoint19_3Input) =>
   raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-const adaptGroup18 = (raw: RawClient["server.question"]) => ({
-  listRequests: Endpoint18_0(raw),
-  list: Endpoint18_1(raw),
-  reply: Endpoint18_2(raw),
-  reject: Endpoint18_3(raw),
+const adaptGroup19 = (raw: RawClient["server.question"]) => ({
+  listRequests: Endpoint19_0(raw),
+  list: Endpoint19_1(raw),
+  reply: Endpoint19_2(raw),
+  reject: Endpoint19_3(raw),
 })
 
-type Endpoint19_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
-type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
-const Endpoint19_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint19_0Input) =>
+type Endpoint20_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
+type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
+const Endpoint20_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint20_0Input) =>
   raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup19 = (raw: RawClient["server.reference"]) => ({ list: Endpoint19_0(raw) })
+const adaptGroup20 = (raw: RawClient["server.reference"]) => ({ list: Endpoint20_0(raw) })
 
-type Endpoint20_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
-type Endpoint20_0Input = {
-  readonly projectID: Endpoint20_0Request["params"]["projectID"]
-  readonly location?: Endpoint20_0Request["query"]["location"]
-  readonly strategy: Endpoint20_0Request["payload"]["strategy"]
-  readonly directory: Endpoint20_0Request["payload"]["directory"]
-  readonly name?: Endpoint20_0Request["payload"]["name"]
+type Endpoint21_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
+type Endpoint21_0Input = {
+  readonly projectID: Endpoint21_0Request["params"]["projectID"]
+  readonly location?: Endpoint21_0Request["query"]["location"]
+  readonly strategy: Endpoint21_0Request["payload"]["strategy"]
+  readonly directory: Endpoint21_0Request["payload"]["directory"]
+  readonly name?: Endpoint21_0Request["payload"]["name"]
 }
-const Endpoint20_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint20_0Input) =>
+const Endpoint21_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint21_0Input) =>
   raw["projectCopy.create"]({
     params: { projectID: input["projectID"] },
     query: { location: input["location"] },
     payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint20_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
-type Endpoint20_1Input = {
-  readonly projectID: Endpoint20_1Request["params"]["projectID"]
-  readonly location?: Endpoint20_1Request["query"]["location"]
-  readonly directory: Endpoint20_1Request["payload"]["directory"]
-  readonly force: Endpoint20_1Request["payload"]["force"]
+type Endpoint21_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
+type Endpoint21_1Input = {
+  readonly projectID: Endpoint21_1Request["params"]["projectID"]
+  readonly location?: Endpoint21_1Request["query"]["location"]
+  readonly directory: Endpoint21_1Request["payload"]["directory"]
+  readonly force: Endpoint21_1Request["payload"]["force"]
 }
-const Endpoint20_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint20_1Input) =>
+const Endpoint21_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint21_1Input) =>
   raw["projectCopy.remove"]({
     params: { projectID: input["projectID"] },
     query: { location: input["location"] },
     payload: { directory: input["directory"], force: input["force"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint20_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
-type Endpoint20_2Input = {
-  readonly projectID: Endpoint20_2Request["params"]["projectID"]
-  readonly location?: Endpoint20_2Request["query"]["location"]
+type Endpoint21_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
+type Endpoint21_2Input = {
+  readonly projectID: Endpoint21_2Request["params"]["projectID"]
+  readonly location?: Endpoint21_2Request["query"]["location"]
 }
-const Endpoint20_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint20_2Input) =>
+const Endpoint21_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint21_2Input) =>
   raw["projectCopy.refresh"]({
     params: { projectID: input["projectID"] },
     query: { location: input["location"] },
   }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup20 = (raw: RawClient["server.projectCopy"]) => ({
-  create: Endpoint20_0(raw),
-  remove: Endpoint20_1(raw),
-  refresh: Endpoint20_2(raw),
+const adaptGroup21 = (raw: RawClient["server.projectCopy"]) => ({
+  create: Endpoint21_0(raw),
+  remove: Endpoint21_1(raw),
+  refresh: Endpoint21_2(raw),
 })
 
 const adaptClient = (raw: RawClient) => ({
@@ -826,18 +833,19 @@ const adaptClient = (raw: RawClient) => ({
   generate: adaptGroup6(raw["server.generate"]),
   provider: adaptGroup7(raw["server.provider"]),
   integration: adaptGroup8(raw["server.integration"]),
-  credential: adaptGroup9(raw["server.credential"]),
-  project: adaptGroup10(raw["server.project"]),
-  permission: adaptGroup11(raw["server.permission"]),
-  file: adaptGroup12(raw["server.fs"]),
-  command: adaptGroup13(raw["server.command"]),
-  skill: adaptGroup14(raw["server.skill"]),
-  event: adaptGroup15(raw["server.event"]),
-  pty: adaptGroup16(raw["server.pty"]),
-  shell: adaptGroup17(raw["server.shell"]),
-  question: adaptGroup18(raw["server.question"]),
-  reference: adaptGroup19(raw["server.reference"]),
-  projectCopy: adaptGroup20(raw["server.projectCopy"]),
+  "server.mcp": adaptGroup9(raw["server.mcp"]),
+  credential: adaptGroup10(raw["server.credential"]),
+  project: adaptGroup11(raw["server.project"]),
+  permission: adaptGroup12(raw["server.permission"]),
+  file: adaptGroup13(raw["server.fs"]),
+  command: adaptGroup14(raw["server.command"]),
+  skill: adaptGroup15(raw["server.skill"]),
+  event: adaptGroup16(raw["server.event"]),
+  pty: adaptGroup17(raw["server.pty"]),
+  shell: adaptGroup18(raw["server.shell"]),
+  question: adaptGroup19(raw["server.question"]),
+  reference: adaptGroup20(raw["server.reference"]),
+  projectCopy: adaptGroup21(raw["server.projectCopy"]),
 })
 
 export const make = (options?: { readonly baseUrl?: URL | string }) =>

+ 16 - 0
packages/client/src/generated/client.ts

@@ -67,6 +67,8 @@ import type {
   IntegrationAttemptCompleteOutput,
   IntegrationAttemptCancelInput,
   IntegrationAttemptCancelOutput,
+  ServerMcpListInput,
+  ServerMcpListOutput,
   CredentialUpdateInput,
   CredentialUpdateOutput,
   CredentialRemoveInput,
@@ -713,6 +715,20 @@ export function make(options: ClientOptions) {
           requestOptions,
         ),
     },
+    "server.mcp": {
+      list: (input?: ServerMcpListInput, requestOptions?: RequestOptions) =>
+        request<ServerMcpListOutput>(
+          {
+            method: "GET",
+            path: `/api/mcp`,
+            query: { location: input?.["location"] },
+            successStatus: 200,
+            declaredStatuses: [401, 400],
+            empty: false,
+          },
+          requestOptions,
+        ),
+    },
     credential: {
       update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
         request<CredentialUpdateOutput>(

+ 24 - 0
packages/client/src/generated/types.ts

@@ -2466,6 +2466,30 @@ export type IntegrationAttemptCancelInput = {
 
 export type IntegrationAttemptCancelOutput = void
 
+export type ServerMcpListInput = {
+  readonly location?: {
+    readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
+  }["location"]
+}
+
+export type ServerMcpListOutput = {
+  readonly location: {
+    readonly directory: string
+    readonly workspaceID?: string
+    readonly project: { readonly id: string; readonly directory: string }
+  }
+  readonly data: ReadonlyArray<{
+    readonly name: string
+    readonly status:
+      | { readonly status: "connected" }
+      | { readonly status: "disconnected" }
+      | { readonly status: "disabled" }
+      | { readonly status: "failed"; readonly error: string }
+      | { readonly status: "needs_auth" }
+      | { readonly status: "needs_client_registration"; readonly error: string }
+  }>
+}
+
 export type CredentialUpdateInput = {
   readonly credentialID: { readonly credentialID: string }["credentialID"]
   readonly location?: {

+ 1 - 0
packages/core/package.json

@@ -86,6 +86,7 @@
     "@effect/platform-node": "catalog:",
     "@effect/sql-sqlite-bun": "catalog:",
     "@lydell/node-pty": "catalog:",
+    "@modelcontextprotocol/sdk": "1.29.0",
     "@ff-labs/fff-bun": "0.9.4",
     "@npmcli/arborist": "9.4.0",
     "@npmcli/config": "10.8.1",

+ 4 - 0
packages/core/src/location-services.ts

@@ -16,6 +16,7 @@ import { Integration } from "./integration"
 import { Location } from "./location"
 import { LocationMutation } from "./location-mutation"
 import { LocationServiceMap } from "./location-service-map"
+import { MCP } from "./mcp/index"
 import { PermissionV2 } from "./permission"
 import { PluginV2 } from "./plugin"
 import { PluginInternal } from "./plugin/internal"
@@ -37,6 +38,7 @@ import { Snapshot } from "./snapshot"
 import { SystemContextBuiltIns } from "./system-context/builtins"
 import { SystemContextRegistry } from "./system-context/registry"
 import { BuiltInTools } from "./tool/builtins"
+import { McpTool } from "./tool/mcp"
 import { ReadToolFileSystem } from "./tool/read-filesystem"
 import { ToolRegistry } from "./tool/registry"
 import { ToolOutputStore } from "./tool-output-store"
@@ -68,6 +70,7 @@ export const locationServices = LayerNode.group([
   SystemContextBuiltIns.node,
   LocationMutation.node,
   FileMutation.node,
+  MCP.node,
   PermissionV2.node,
   ToolOutputStore.node,
   ToolRegistry.node,
@@ -80,6 +83,7 @@ export const locationServices = LayerNode.group([
   Generate.node,
   ReadToolFileSystem.node,
   BuiltInTools.node,
+  McpTool.node,
   SessionRunnerModel.node,
   SessionCompaction.node,
   Snapshot.node,

+ 289 - 0
packages/core/src/mcp/client.ts

@@ -0,0 +1,289 @@
+export * as MCPClient from "./client"
+
+import path from "node:path"
+import { execFile } from "node:child_process"
+import { pathToFileURL } from "node:url"
+import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js"
+import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
+import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
+import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
+import {
+  CallToolResultSchema,
+  ListRootsRequestSchema,
+  ListToolsResultSchema,
+  type LoggingMessageNotification,
+  LoggingMessageNotificationSchema,
+  ToolListChangedNotificationSchema,
+  ToolSchema,
+} from "@modelcontextprotocol/sdk/types.js"
+import { Cause, Effect, Exit, Schema } from "effect"
+import { ConfigMCP } from "../config/mcp"
+import { InstallationVersion } from "../installation/version"
+
+const DEFAULT_STARTUP_TIMEOUT = 30_000
+const DEFAULT_REQUEST_TIMEOUT = 30_000
+
+type Transport = StdioClientTransport | StreamableHTTPClientTransport
+
+// Some servers advertise tool outputSchemas the SDK's strict validator can't resolve; this drops
+// only that field so a single bad schema doesn't blank out the whole tool list.
+const TolerantListToolsResult = ListToolsResultSchema.extend({
+  tools: ToolSchema.omit({ outputSchema: true }).array(),
+})
+
+export class NeedsAuthError extends Schema.TaggedErrorClass<NeedsAuthError>()("MCP.NeedsAuthError", {
+  server: Schema.String,
+}) {}
+
+export class ConnectError extends Schema.TaggedErrorClass<ConnectError>()("MCP.ConnectError", {
+  server: Schema.String,
+  message: Schema.String,
+}) {}
+
+export interface ToolDefinition {
+  readonly name: string
+  readonly description: string | undefined
+  readonly inputSchema: unknown
+}
+
+export type CallToolContent =
+  | { readonly type: "text"; readonly text: string }
+  | { readonly type: "media"; readonly data: string; readonly mimeType: string }
+
+export interface CallToolResult {
+  readonly isError: boolean
+  readonly structured: unknown
+  readonly content: ReadonlyArray<CallToolContent>
+}
+
+export interface LogMessage {
+  readonly level: LoggingMessageNotification["params"]["level"]
+  readonly logger?: LoggingMessageNotification["params"]["logger"]
+  readonly data: LoggingMessageNotification["params"]["data"]
+}
+
+/** Handle over a connected MCP server that keeps the SDK `Client` out of the rest of core. */
+export interface Connection {
+  /** Server-supplied usage instructions from the initialize result, if any. */
+  readonly instructions: string | undefined
+  /** Lists the server's tools; returns [] when the server doesn't advertise tool support, fails on a transport error. */
+  readonly tools: () => Effect.Effect<ToolDefinition[], Error>
+  /** Invokes a tool on the server. Interruption aborts the in-flight request. */
+  readonly callTool: (input: {
+    readonly name: string
+    readonly args?: Record<string, unknown>
+  }) => Effect.Effect<CallToolResult, Error>
+  readonly onClose: (callback: () => void) => void
+  /** Registers a callback fired when the server emits an MCP logging notification. */
+  readonly onLog: (callback: (message: LogMessage) => void) => void
+  /** Registers a callback fired when the server announces its tool list changed; no-op if unsupported. */
+  readonly onToolsChanged: (callback: () => void) => void
+}
+
+/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
+export const connect = Effect.fnUntraced(function* (
+  server: string,
+  config: typeof ConfigMCP.Server.Type,
+  directory: string,
+  // Only consumed by the remote transport; stdio servers have no auth concept. A provider with no
+  // stored token (and a no-op redirect) surfaces an UnauthorizedError, which we map to needs_auth.
+  authProvider?: OAuthClientProvider,
+) {
+  const transport: Transport = yield* Effect.gen(function* () {
+    if (config.type === "local") {
+      const [command, ...args] = config.command
+      return new StdioClientTransport({
+        command,
+        args,
+        cwd: config.cwd ? path.resolve(directory, config.cwd) : directory,
+        stderr: "pipe",
+        env: {
+          ...(process.env as Record<string, string>),
+          ...(command === "opencode" ? { BUN_BE_BUN: "1" } : {}),
+          ...config.environment,
+        },
+      })
+    }
+    if (!URL.canParse(config.url)) return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` })
+    return new StreamableHTTPClientTransport(new URL(config.url), {
+      requestInit: config.headers ? { headers: config.headers } : undefined,
+      authProvider,
+    })
+  })
+  const client = new Client(
+    { name: "opencode", version: InstallationVersion },
+    {
+      capabilities: {
+        // https://github.com/anomalyco/opencode/issues/2308
+        roots: {},
+      },
+    },
+  )
+  client.setRequestHandler(ListRootsRequestSchema, () =>
+    Promise.resolve({ roots: [{ uri: pathToFileURL(directory).href }] }),
+  )
+
+  const exit = yield* Effect.tryPromise({
+    try: (signal) => client.connect(transport, { timeout: config.timeout?.startup ?? DEFAULT_STARTUP_TIMEOUT, signal }),
+    catch: (error) => error,
+  }).pipe(Effect.exit)
+  if (Exit.isSuccess(exit)) {
+    yield* Effect.addFinalizer(() =>
+      cleanupStdioDescendants(transport).pipe(
+        Effect.andThen(Effect.promise(() => client.close())),
+        Effect.ignore,
+      ),
+    )
+    const requestTimeout = config.timeout?.request ?? DEFAULT_REQUEST_TIMEOUT
+    return {
+      instructions: client.getInstructions()?.trim() || undefined,
+      tools: () =>
+        Effect.gen(function* () {
+          if (!client.getServerCapabilities()?.tools) return []
+          const tools = yield* Effect.tryPromise({
+            try: () =>
+              paginate(
+                async (cursor) => {
+                  const params = cursor === undefined ? undefined : { cursor }
+                  try {
+                    return await client.listTools(params, { timeout: requestTimeout })
+                  } catch (error) {
+                    if (!(error instanceof Error) || !isOutputSchemaError(error)) throw error
+                    return client.request({ method: "tools/list", params }, TolerantListToolsResult, {
+                      timeout: requestTimeout,
+                    })
+                  }
+                },
+                (result) => result.tools,
+              ),
+            catch: (error) => (error instanceof Error ? error : new Error(String(error))),
+          }).pipe(
+            Effect.tapError((error) => Effect.logWarning("failed to list MCP tools", { server, error: error.message })),
+          )
+          return tools.map((tool) => ({
+            name: tool.name,
+            description: tool.description,
+            inputSchema: tool.inputSchema,
+          }))
+        }),
+      callTool: (input) =>
+        Effect.tryPromise({
+          try: (signal) =>
+            client.callTool(
+              { name: input.name, arguments: input.args ?? {} },
+              CallToolResultSchema,
+              // The SDK only sends a progress token when onprogress is present, which enables timeout resets.
+              { signal, timeout: requestTimeout, resetTimeoutOnProgress: true, onprogress: () => {} },
+            ),
+          catch: (error) => (error instanceof Error ? error : new Error(String(error))),
+        }).pipe(
+          Effect.map((result) => ({
+            isError: result.isError === true,
+            structured: result.structuredContent,
+            content: result.content.flatMap((part): CallToolContent[] => {
+              if (part.type === "text") return [{ type: "text", text: part.text }]
+              if (part.type === "image" || part.type === "audio")
+                return [{ type: "media", data: part.data, mimeType: part.mimeType }]
+              if (part.type === "resource_link") return [{ type: "text", text: part.uri }]
+              if (part.type === "resource") {
+                const resource = part.resource
+                if ("text" in resource && typeof resource.text === "string")
+                  return [{ type: "text", text: resource.text }]
+                if ("blob" in resource && typeof resource.blob === "string" && typeof resource.mimeType === "string")
+                  return [{ type: "media", data: resource.blob, mimeType: resource.mimeType }]
+                return [{ type: "text", text: resource.uri }]
+              }
+              return []
+            }),
+          })),
+        ),
+      onClose: (callback) => {
+        client.onclose = callback
+      },
+      onLog: (callback) => {
+        client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => callback(notification.params))
+      },
+      onToolsChanged: (callback) => {
+        if (!client.getServerCapabilities()?.tools?.listChanged) return
+        client.setNotificationHandler(ToolListChangedNotificationSchema, async () => callback())
+      },
+    } satisfies Connection
+  }
+
+  yield* cleanupStdioDescendants(transport).pipe(
+    Effect.andThen(Effect.promise(() => transport.close())),
+    Effect.ignore,
+  )
+  const error = Cause.squash(exit.cause)
+  if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server })
+  return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) })
+})
+
+// SDK close stops the MCP process, but not child processes it spawned.
+const cleanupStdioDescendants = (transport: Transport) =>
+  Effect.gen(function* () {
+    if (!(transport instanceof StdioClientTransport)) return
+    const pid = transport.pid
+    if (typeof pid !== "number") return
+    yield* Effect.forEach(
+      yield* descendantPids(pid),
+      (pid) =>
+        Effect.try({
+          try: () => process.kill(pid, "SIGTERM"),
+          catch: () => undefined,
+        }).pipe(Effect.ignore),
+      { discard: true },
+    )
+  })
+
+const descendantPids = Effect.fnUntraced(function* (root: number) {
+  if (process.platform === "win32") return []
+  const result: number[] = []
+  const queue = [root]
+  for (let index = 0; index < queue.length; index++) {
+    const parent = queue[index]
+    if (parent === undefined) return result
+    const children = (yield* childPids(parent)).filter((pid) => !result.includes(pid))
+    result.push(...children)
+    queue.push(...children)
+  }
+  return result
+})
+
+const childPids = (pid: number) =>
+  Effect.promise(
+    () =>
+      new Promise<number[]>((resolve) => {
+        execFile("pgrep", ["-P", String(pid)], { encoding: "utf8" }, (_error, stdout) => {
+          resolve(
+            stdout
+              .split("\n")
+              .map((line) => Number.parseInt(line, 10))
+              .filter((pid) => Number.isInteger(pid)),
+          )
+        })
+      }),
+  )
+
+async function paginate<R extends { nextCursor?: string }, T>(
+  list: (cursor: string | undefined) => Promise<R>,
+  items: (result: R) => T[],
+) {
+  const collected: T[] = []
+  const seen = new Set<string>()
+  let cursor: string | undefined
+  while (true) {
+    const result = await list(cursor)
+    collected.push(...items(result))
+    if (result.nextCursor === undefined) return collected
+    // A repeating cursor never terminates; bail instead of hanging the connection forever.
+    if (seen.has(result.nextCursor)) throw new Error(`MCP list returned duplicate cursor: ${result.nextCursor}`)
+    seen.add(result.nextCursor)
+    cursor = result.nextCursor
+  }
+}
+
+const isOutputSchemaError = (error: Error) =>
+  /can't resolve reference|resolves to more than one schema|outputSchema|schema.*reference|reference.*schema/i.test(
+    error.message,
+  )

+ 78 - 0
packages/core/src/mcp/guidance.ts

@@ -0,0 +1,78 @@
+export * as McpGuidance from "./guidance"
+
+import { makeLocationNode } from "../effect/app-node"
+import { Context, Effect, Layer, Schema } from "effect"
+import { AgentV2 } from "../agent"
+import { PermissionV2 } from "../permission"
+import { McpTool } from "../tool/mcp"
+import { MCP } from "./index"
+import { SystemContext } from "../system-context/index"
+
+const Summary = Schema.Struct({
+  server: Schema.String,
+  instructions: Schema.String,
+})
+type Summary = typeof Summary.Type
+
+const render = (servers: ReadonlyArray<Summary>) =>
+  [
+    "<mcp_instructions>",
+    ...servers.flatMap((server) => [
+      `  <server name="${server.server}">`,
+      ...server.instructions.split("\n").map((line) => `    ${line}`),
+      "  </server>",
+    ]),
+    "</mcp_instructions>",
+  ].join("\n")
+
+export interface Interface {
+  readonly load: (agent: AgentV2.Selection) => Effect.Effect<SystemContext.SystemContext>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/v2/McpGuidance") {}
+
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const mcp = yield* MCP.Service
+
+    return Service.of({
+      load: Effect.fn("McpGuidance.load")(function* (selection) {
+        const agent = selection.info
+        if (!agent) return SystemContext.empty
+        const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], {
+          concurrency: "unbounded",
+        })
+        // Hide a server only when every tool it contributes is wholly denied for this agent.
+        const visible = instructions
+          .filter((item) => {
+            const owned = tools.filter((tool) => tool.server === item.server)
+            return (
+              owned.length === 0 ||
+              owned.some(
+                (tool) => PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
+              )
+            )
+          })
+          .map((item) => ({ server: item.server, instructions: item.instructions }))
+        if (visible.length === 0) return SystemContext.empty
+        return SystemContext.make({
+          key: SystemContext.Key.make("core/mcp-guidance"),
+          codec: Schema.toCodecJson(Schema.Array(Summary)),
+          load: Effect.succeed(visible),
+          baseline: render,
+          update: (_previous, current) =>
+            [
+              "The available MCP server instructions have changed. This list supersedes the previous one.",
+              render(current),
+            ].join("\n"),
+          removed: () => "MCP server instructions are no longer available.",
+        })
+      }),
+    })
+  }),
+)
+
+export const locationLayer = layer
+
+export const node = makeLocationNode({ service: Service, layer, deps: [MCP.node] })

+ 498 - 0
packages/core/src/mcp/index.ts

@@ -0,0 +1,498 @@
+export * as MCP from "./index"
+
+import { Mcp } from "@opencode-ai/schema/mcp"
+import { McpEvent } from "@opencode-ai/schema/mcp-event"
+import { createHash } from "node:crypto"
+import { Cause, Context, Deferred, Effect, Exit, FiberSet, Layer, Schema, Scope, Stream } from "effect"
+import { makeLocationNode } from "../effect/app-node"
+import { Config } from "../config"
+import { ConfigMCP } from "../config/mcp"
+import { Credential } from "../credential"
+import { EventV2 } from "../event"
+import { Integration } from "../integration"
+import { IntegrationConnection } from "../integration/connection"
+import { Location } from "../location"
+import { MCPClient } from "./client"
+import { MCPOAuth } from "./oauth"
+
+export const ServerName = Schema.String.pipe(Schema.brand("MCP.ServerName"))
+export type ServerName = typeof ServerName.Type
+
+// The status union is a public wire contract, so it lives in @opencode-ai/schema and is re-exported here.
+export const Status = Mcp.Status
+export type Status = Mcp.Status
+
+export class ServerInfo extends Schema.Class<ServerInfo>("MCP.ServerInfo")({
+  name: ServerName,
+  status: Status,
+  integrationID: Integration.ID.pipe(Schema.optional),
+  connection: IntegrationConnection.Info.pipe(Schema.optional),
+}) {}
+
+export class ServerInstructions extends Schema.Class<ServerInstructions>("MCP.ServerInstructions")({
+  server: ServerName,
+  instructions: Schema.String,
+}) {}
+
+export class Tool extends Schema.Class<Tool>("MCP.Tool")({
+  server: ServerName,
+  name: Schema.String,
+  description: Schema.String.pipe(Schema.optional),
+  inputSchema: Schema.Unknown.pipe(Schema.optional),
+}) {}
+
+export const ToolResultContent = Schema.Union([
+  Schema.Struct({ type: Schema.Literal("text"), text: Schema.String }),
+  Schema.Struct({ type: Schema.Literal("media"), data: Schema.String, mimeType: Schema.String }),
+]).pipe(Schema.toTaggedUnion("type"))
+export type ToolResultContent = typeof ToolResultContent.Type
+
+export class ToolResult extends Schema.Class<ToolResult>("MCP.ToolResult")({
+  server: ServerName,
+  tool: Schema.String,
+  isError: Schema.Boolean,
+  structured: Schema.Unknown.pipe(Schema.optional),
+  content: Schema.Array(ToolResultContent),
+}) {}
+
+export class PromptArgument extends Schema.Class<PromptArgument>("MCP.PromptArgument")({
+  name: Schema.String,
+  description: Schema.String.pipe(Schema.optional),
+  required: Schema.Boolean.pipe(Schema.optional),
+}) {}
+
+export class Prompt extends Schema.Class<Prompt>("MCP.Prompt")({
+  server: ServerName,
+  name: Schema.String,
+  description: Schema.String.pipe(Schema.optional),
+  arguments: Schema.Array(PromptArgument).pipe(Schema.optional),
+}) {}
+
+export class PromptMessage extends Schema.Class<PromptMessage>("MCP.PromptMessage")({
+  role: Schema.String,
+  content: Schema.Unknown,
+}) {}
+
+export class PromptResult extends Schema.Class<PromptResult>("MCP.PromptResult")({
+  server: ServerName,
+  name: Schema.String,
+  messages: Schema.Array(PromptMessage),
+}) {}
+
+export class Resource extends Schema.Class<Resource>("MCP.Resource")({
+  server: ServerName,
+  name: Schema.String,
+  uri: Schema.String,
+  description: Schema.String.pipe(Schema.optional),
+  mimeType: Schema.String.pipe(Schema.optional),
+}) {}
+
+export class ResourceTemplate extends Schema.Class<ResourceTemplate>("MCP.ResourceTemplate")({
+  server: ServerName,
+  name: Schema.String,
+  uriTemplate: Schema.String,
+  description: Schema.String.pipe(Schema.optional),
+  mimeType: Schema.String.pipe(Schema.optional),
+}) {}
+
+export class ResourceCatalog extends Schema.Class<ResourceCatalog>("MCP.ResourceCatalog")({
+  resources: Schema.Array(Resource),
+  templates: Schema.Array(ResourceTemplate),
+}) {}
+
+export const ResourceContentPart = Schema.Union([
+  Schema.Struct({
+    type: Schema.Literal("text"),
+    uri: Schema.String,
+    text: Schema.String,
+    mimeType: Schema.String.pipe(Schema.optional),
+  }),
+  Schema.Struct({
+    type: Schema.Literal("blob"),
+    uri: Schema.String,
+    blob: Schema.String,
+    mimeType: Schema.String.pipe(Schema.optional),
+  }),
+]).pipe(Schema.toTaggedUnion("type"))
+export type ResourceContentPart = typeof ResourceContentPart.Type
+
+export class ResourceContent extends Schema.Class<ResourceContent>("MCP.ResourceContent")({
+  server: ServerName,
+  uri: Schema.String,
+  contents: Schema.Array(ResourceContentPart),
+}) {}
+
+export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
+  server: ServerName,
+}) {}
+
+export class ToolCallError extends Schema.TaggedErrorClass<ToolCallError>()("MCP.ToolCallError", {
+  server: ServerName,
+  tool: Schema.String,
+  message: Schema.String,
+}) {}
+
+type ServerEntry = {
+  readonly config: typeof ConfigMCP.Server.Type
+  status: Status
+  readonly startup: Deferred.Deferred<void>
+  scope?: Scope.Closeable
+  client?: MCPClient.Connection
+  tools?: ReadonlyArray<Tool>
+  // Set when a remote server is registered as an OAuth integration; the credential lives in the global store.
+  integrationID?: Integration.ID
+}
+
+export interface Interface {
+  readonly servers: () => Effect.Effect<ServerInfo[]>
+  readonly tools: () => Effect.Effect<Tool[]>
+  readonly callTool: (input: {
+    readonly server: ServerName | string
+    readonly name: string
+    readonly args?: Record<string, unknown>
+  }) => Effect.Effect<ToolResult, NotFoundError | ToolCallError>
+  readonly instructions: () => Effect.Effect<ServerInstructions[]>
+  readonly prompts: () => Effect.Effect<Prompt[]>
+  readonly prompt: (input: {
+    readonly server: ServerName | string
+    readonly name: string
+    readonly args?: Record<string, string>
+  }) => Effect.Effect<PromptResult | undefined, NotFoundError>
+  readonly resourceCatalog: () => Effect.Effect<ResourceCatalog>
+  readonly readResource: (input: {
+    readonly server: ServerName | string
+    readonly uri: string
+  }) => Effect.Effect<ResourceContent | undefined, NotFoundError>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/v2/MCP") {}
+
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const config = yield* Config.Service
+    const location = yield* Location.Service
+    const events = yield* EventV2.Service
+    const integration = yield* Integration.Service
+    const credentials = yield* Credential.Service
+    const root = yield* Scope.make()
+    const fork = yield* FiberSet.makeRuntime<never, void, never>()
+    yield* Effect.addFinalizer((exit) => Scope.close(root, exit))
+
+    const documents = (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document")
+    // Global MCP timeout defaults, later config files overriding earlier ones.
+    const timeout = Object.assign(
+      {},
+      ...documents.flatMap((entry) => (entry.info.mcp?.timeout ? [entry.info.mcp.timeout] : [])),
+    )
+    // Later config files win for duplicate server names; per-server timeout overrides globals.
+    const runtime = new Map<ServerName, ServerEntry>()
+    for (const entry of documents) {
+      for (const [name, server] of Object.entries(entry.info.mcp?.servers ?? {})) {
+        runtime.set(ServerName.make(name), {
+          config: { ...server, timeout: { ...timeout, ...server.timeout } },
+          status: { status: "disconnected" },
+          startup: Deferred.makeUnsafe<void>(),
+        })
+      }
+    }
+
+    // Register every remote server as an OAuth integration so credentials live in the global store
+    // rather than in committed config. Servers that connect anonymously simply never use the method.
+    const registrations: Array<{
+      readonly name: ServerName
+      readonly remote: typeof ConfigMCP.Remote.Type
+      readonly integrationID: Integration.ID
+      readonly methodID: Integration.MethodID
+    }> = []
+    for (const [name, entry] of runtime) {
+      if (entry.config.type !== "remote" || entry.config.oauth === false) continue
+      const remote = entry.config
+      // Key identity on name + url, not url alone: two configs for the same url under different names are
+      // distinct logical servers that may hold different accounts, so they must not share a credential row.
+      const suffix = "mcp_" + createHash("sha1").update(name + "\u0000" + remote.url).digest("hex").slice(0, 16)
+      entry.integrationID = Integration.ID.make(suffix)
+      registrations.push({ name, remote, integrationID: entry.integrationID, methodID: Integration.MethodID.make(suffix) })
+    }
+    if (registrations.length > 0)
+      yield* integration.transform((draft) => {
+        for (const reg of registrations) {
+          draft.update(reg.integrationID, (ref) => {
+            ref.name = reg.name
+          })
+          draft.method.update({
+            integrationID: reg.integrationID,
+            method: { id: reg.methodID, type: "oauth", label: reg.name },
+            authorize: () => MCPOAuth.authorize({ name: reg.name, config: reg.remote, methodID: reg.methodID }),
+          })
+        }
+      })
+
+    const requireServer = Effect.fnUntraced(function* (server: ServerName | string) {
+      const name = ServerName.make(server)
+      const entry = runtime.get(name)
+      if (!entry) return yield* new NotFoundError({ server: name })
+      return { name, entry }
+    })
+
+    const info = (name: ServerName, entry: ServerEntry, connection: IntegrationConnection.Info | undefined) =>
+      new ServerInfo({
+        name,
+        status: entry.status,
+        integrationID: entry.integrationID,
+        connection,
+      })
+
+    // Builds the connect-time auth provider for a remote OAuth-integration server. The SDK presents and
+    // refreshes stored tokens, persisting refreshes back to the same credential row. The provider never
+    // opens a browser, so an auth-gated connect ends in UnauthorizedError -> needs_auth rather than a redirect.
+    const connectProvider = Effect.fnUntraced(function* (entry: ServerEntry) {
+      if (entry.config.type !== "remote" || !entry.integrationID) return undefined
+      const remote = entry.config
+      const oauth = remote.oauth || undefined
+      const base = {
+        redirectUrl: oauth?.redirect_uri ?? "http://127.0.0.1/callback",
+        scope: oauth?.scope,
+        client: oauth?.client_id ? { id: oauth.client_id, secret: oauth.client_secret } : undefined,
+        // No browser during connect: an auth-gated server surfaces needs_auth instead of opening a browser.
+        onRedirect: () => {},
+      }
+      const stored = yield* credentials.list(entry.integrationID)
+      const found = stored.find((credential) => credential.value.type === "oauth")
+      if (!found || found.value.type !== "oauth")
+        // No stored credential yet: an empty in-memory store still lets the SDK run the auth handshake, which
+        // ends in UnauthorizedError -> needs_auth. Returning no provider instead would let the transport throw
+        // a raw HTTP error, hiding the auth requirement behind a generic failed status. Anonymous servers are
+        // unaffected: tokens() returns undefined, so no auth header is sent and the SDK never calls auth().
+        return MCPOAuth.provider({ ...base, store: MCPOAuth.memoryStore() })
+      const credentialID = found.id
+      const methodID = found.value.methodID
+      let current: Credential.OAuth | undefined = found.value
+      return MCPOAuth.provider({
+        ...base,
+        // Drop a credential the SDK rejected so the next connect cleanly reports needs_auth. Uses the raw
+        // credential service (no integration event) to avoid re-triggering the reconnect subscriber mid-connect.
+        invalidate: async (scope) => {
+          if (scope === "verifier" || scope === "discovery") return
+          current = undefined
+          await Effect.runPromise(credentials.remove(credentialID))
+        },
+        store: {
+          tokens: async () => (current ? MCPOAuth.toTokens(current) : undefined),
+          saveTokens: async (tokens) => {
+            current = MCPOAuth.toCredential({
+              methodID,
+              serverUrl: remote.url,
+              tokens,
+              client: current ? MCPOAuth.clientFromCredential(current) : undefined,
+            })
+            await Effect.runPromise(credentials.update(credentialID, { value: current }))
+          },
+          clientInformation: async () => (current ? MCPOAuth.clientFromCredential(current) : undefined),
+          saveClientInformation: async () => {},
+          codeVerifier: async () => undefined,
+          saveCodeVerifier: async () => {},
+        },
+      })
+    })
+
+    const toTool = (server: ServerName, def: MCPClient.ToolDefinition) =>
+      new Tool({ server, name: def.name, description: def.description, inputSchema: def.inputSchema })
+
+    const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
+      connection.tools().pipe(
+        Effect.map((defs) => {
+          entry.tools = defs.map((def) => toTool(name, def))
+        }),
+      )
+
+    const watch = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) => {
+      connection.onClose(() => {
+          entry.client = undefined
+          entry.tools = undefined
+          entry.status = { status: "failed", error: "Connection closed" }
+          fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore))
+          fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore))
+      })
+      connection.onLog((message) => fork(serverLog(name, message).pipe(Effect.ignore)))
+      connection.onToolsChanged(() => {
+        fork(
+          refreshTools(name, entry, connection).pipe(
+            Effect.andThen(events.publish(McpEvent.ToolsChanged, { server: name })),
+            Effect.ignore,
+          ),
+        )
+      })
+    }
+
+    const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
+      const fields = { server, logger: message.logger, level: message.level, data: message.data }
+      switch (message.level) {
+        case "debug":
+          return Effect.logDebug("MCP server log", fields)
+        case "info":
+        case "notice":
+          return Effect.logInfo("MCP server log", fields)
+        case "warning":
+          return Effect.logWarning("MCP server log", fields)
+        case "error":
+        case "critical":
+        case "alert":
+        case "emergency":
+          return Effect.logError("MCP server log", fields)
+      }
+    }
+
+    const startServer = (name: ServerName, entry: ServerEntry) =>
+      Effect.gen(function* () {
+        const scope = yield* Scope.fork(root)
+        entry.scope = scope
+        const authProvider = yield* connectProvider(entry)
+        // List tools as part of connect so a failure here marks the server failed rather than
+        // leaving it connected with a silently empty tool list and no path to recover.
+        const result = yield* MCPClient.connect(name, entry.config, location.directory, authProvider).pipe(
+          Effect.flatMap((connection) => connection.tools().pipe(Effect.map((defs) => ({ connection, defs })))),
+          Scope.provide(scope),
+          Effect.exit,
+        )
+        if (Exit.isSuccess(result)) {
+          entry.client = result.value.connection
+          entry.tools = result.value.defs.map((def) => toTool(name, def))
+          entry.status = { status: "connected" }
+          watch(name, entry, result.value.connection)
+          yield* Effect.logInfo("mcp connected", { server: name, tools: entry.tools.length })
+          // Announce the new tool set so the tool registry registers it. A server that finishes connecting
+          // after the initial registration sweep and emits no list-changed notification would otherwise
+          // stay invisible to the model.
+          yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
+          yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
+          return
+        }
+        yield* Scope.close(scope, Exit.void)
+        entry.scope = undefined
+        const error = Cause.squash(result.cause)
+        entry.status =
+          error instanceof MCPClient.NeedsAuthError
+            ? { status: "needs_auth" }
+            : { status: "failed", error: error instanceof Error ? error.message : String(error) }
+        yield* Effect.logWarning("mcp connect failed", { server: name, status: entry.status })
+        yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
+      }).pipe(Effect.ensuring(Deferred.succeed(entry.startup, undefined)))
+
+    // Disabled servers settle their startup immediately so queries never block on them.
+    for (const [name, entry] of runtime) {
+      if (entry.config.disabled) {
+        entry.status = { status: "disabled" }
+        Deferred.doneUnsafe(entry.startup, Exit.void)
+        continue
+      }
+      fork(startServer(name, entry))
+    }
+
+    // Bring a server online (or back to needs_auth) when its integration's credential changes, so an
+    // OAuth login takes effect without a restart. Only fires for the integrations we registered.
+    const owned = new Set(registrations.map((reg) => reg.integrationID))
+    const reconnect = (integrationID: Integration.ID) =>
+      Effect.gen(function* () {
+        const match = Array.from(runtime).find(([, entry]) => entry.integrationID === integrationID)
+        if (!match) return
+        const [name, entry] = match
+        if (entry.config.disabled) return
+        if (entry.scope) {
+          yield* Scope.close(entry.scope, Exit.void)
+          entry.scope = undefined
+          entry.client = undefined
+          entry.tools = undefined
+        }
+        yield* startServer(name, entry)
+      })
+    fork(
+      events.subscribe(Integration.Event.ConnectionUpdated).pipe(
+        Stream.filter((event) => owned.has(event.data.integrationID)),
+        Stream.runForEach((event) => Effect.sync(() => fork(reconnect(event.data.integrationID)))),
+        Effect.ignore,
+      ),
+    )
+
+    const whenAllReady = Effect.forEach(runtime.values(), (entry) => Deferred.await(entry.startup), {
+      concurrency: "unbounded",
+      discard: true,
+    })
+    const gate = Effect.fnUntraced(function* (server: ServerName | string) {
+      const target = yield* requireServer(server)
+      yield* Deferred.await(target.entry.startup)
+    })
+
+    return Service.of({
+      servers: Effect.fn("MCP.servers")(function* () {
+        const entries = Array.from(runtime).toSorted(([a], [b]) => a.localeCompare(b))
+        return yield* Effect.forEach(entries, ([name, entry]) =>
+          Effect.gen(function* () {
+            const connection = entry.integrationID
+              ? yield* integration.connection.active(entry.integrationID)
+              : undefined
+            return info(name, entry, connection)
+          }),
+        )
+      }),
+      tools: Effect.fn("MCP.tools")(function* () {
+        yield* whenAllReady
+        return Array.from(runtime.values())
+          .flatMap((entry) => entry.tools ?? [])
+          .toSorted((a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name))
+      }),
+      callTool: Effect.fn("MCP.callTool")(function* (input) {
+        const target = yield* requireServer(input.server)
+        yield* Deferred.await(target.entry.startup)
+        if (!target.entry.client)
+          return yield* new ToolCallError({
+            server: target.name,
+            tool: input.name,
+            message: "MCP server is not connected",
+          })
+        const result = yield* target.entry.client
+          .callTool({ name: input.name, args: input.args })
+          .pipe(Effect.mapError((error) => new ToolCallError({ server: target.name, tool: input.name, message: error.message })))
+        return new ToolResult({
+          server: target.name,
+          tool: input.name,
+          isError: result.isError,
+          structured: result.structured,
+          content: result.content,
+        })
+      }),
+      instructions: Effect.fn("MCP.instructions")(function* () {
+        yield* whenAllReady
+        return Array.from(runtime)
+          .flatMap(([server, entry]) => {
+            const instructions = entry.client?.instructions
+            if (!instructions) return []
+            return [new ServerInstructions({ server, instructions })]
+          })
+          .toSorted((a, b) => a.server.localeCompare(b.server))
+      }),
+      prompts: Effect.fn("MCP.prompts")(function* () {
+        yield* whenAllReady
+        return []
+      }),
+      prompt: Effect.fn("MCP.prompt")(function* (input) {
+        yield* gate(input.server)
+        return undefined
+      }),
+      resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () {
+        yield* whenAllReady
+        return new ResourceCatalog({ resources: [], templates: [] })
+      }),
+      readResource: Effect.fn("MCP.readResource")(function* (input) {
+        yield* gate(input.server)
+        return undefined
+      }),
+    })
+  }),
+)
+
+export const node = makeLocationNode({
+  service: Service,
+  layer,
+  deps: [Config.node, Location.node, EventV2.node, Integration.node, Credential.node],
+})

+ 238 - 0
packages/core/src/mcp/oauth.ts

@@ -0,0 +1,238 @@
+export * as MCPOAuth from "./oauth"
+
+import { auth, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
+import type { OAuthClientInformationMixed, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"
+import { createServer } from "node:http"
+import { Deferred, Effect } from "effect"
+import { Credential } from "@opencode-ai/schema/credential"
+import { ConfigMCP } from "../config/mcp"
+import { OauthCallbackPage } from "../oauth/page"
+import type { Integration } from "../integration"
+
+/** Persists the OAuth artifacts for one MCP server session: DCR client info, PKCE verifier, and tokens. */
+export interface Store {
+  readonly tokens: () => Promise<OAuthTokens | undefined>
+  readonly saveTokens: (tokens: OAuthTokens) => Promise<void>
+  readonly clientInformation: () => Promise<OAuthClientInformationMixed | undefined>
+  readonly saveClientInformation: (info: OAuthClientInformationMixed) => Promise<void>
+  readonly codeVerifier: () => Promise<string | undefined>
+  readonly saveCodeVerifier: (verifier: string) => Promise<void>
+}
+
+export interface Options {
+  /** Loopback URL the authorization server redirects back to after the user approves. */
+  readonly redirectUrl: string
+  /** Space-delimited OAuth scopes to request when the server requires specific ones. */
+  readonly scope?: string
+  /** CSRF state embedded in the authorization request; required by the spec and enforced by some servers.
+   * The caller is responsible for validating the value echoed back to the redirect. */
+  readonly state?: string
+  /** Statically pre-registered client credentials from config; when set, the SDK skips dynamic registration. */
+  readonly client?: { readonly id: string; readonly secret?: string }
+  /** Invoked by the SDK to drop credentials it has determined are invalid (e.g. a rejected refresh token). */
+  readonly invalidate?: (scope: "all" | "client" | "tokens" | "verifier" | "discovery") => void | Promise<void>
+  /** Receives the authorization URL so the caller can open a browser and capture the eventual code. */
+  readonly onRedirect: (url: URL) => void | Promise<void>
+  readonly store: Store
+}
+
+/**
+ * Builds the MCP SDK's OAuthClientProvider. The SDK drives dynamic client registration, PKCE, and
+ * token refresh through these callbacks; we only persist whatever it hands back via `store`.
+ */
+export const provider = (options: Options): OAuthClientProvider => {
+  const state = options.state
+  const client = options.client
+  return {
+    redirectUrl: options.redirectUrl,
+    clientMetadata: {
+      redirect_uris: [options.redirectUrl],
+      client_name: "opencode",
+      client_uri: "https://opencode.ai",
+      grant_types: ["authorization_code", "refresh_token"],
+      response_types: ["code"],
+      token_endpoint_auth_method: client?.secret ? "client_secret_post" : "none",
+      ...(options.scope ? { scope: options.scope } : {}),
+    },
+    // Only advertise state when the caller supplied one (the interactive flow); the connect-time
+    // provider has no redirect to validate, so it omits it.
+    ...(state !== undefined ? { state: () => state } : {}),
+    // Static client config short-circuits dynamic registration; otherwise the SDK registers and we persist.
+    clientInformation: () =>
+      client ? { client_id: client.id, client_secret: client.secret } : options.store.clientInformation(),
+    saveClientInformation: (info) => options.store.saveClientInformation(info),
+    tokens: () => options.store.tokens(),
+    saveTokens: (tokens) => options.store.saveTokens(tokens),
+    redirectToAuthorization: (url) => options.onRedirect(url),
+    ...(options.invalidate ? { invalidateCredentials: options.invalidate } : {}),
+    saveCodeVerifier: (verifier) => options.store.saveCodeVerifier(verifier),
+    // The SDK only reads the verifier back after saving one earlier in the same flow; a miss means
+    // the flow was resumed without its session state, which the SDK surfaces as an auth failure.
+    codeVerifier: async () => {
+      const verifier = await options.store.codeVerifier()
+      if (!verifier) throw new Error("Missing PKCE code verifier for MCP OAuth flow")
+      return verifier
+    },
+  }
+}
+
+/** A Store that keeps OAuth artifacts in memory for the duration of one interactive login attempt. */
+export const memoryStore = (): Store => {
+  let tokens: OAuthTokens | undefined
+  let client: OAuthClientInformationMixed | undefined
+  let verifier: string | undefined
+  return {
+    tokens: async () => tokens,
+    saveTokens: async (value) => {
+      tokens = value
+    },
+    clientInformation: async () => client,
+    saveClientInformation: async (value) => {
+      client = value
+    },
+    codeVerifier: async () => verifier,
+    saveCodeVerifier: async (value) => {
+      verifier = value
+    },
+  }
+}
+
+/** Reads the dynamically-registered client info we stash in a credential's metadata, for token refresh. */
+export const clientFromCredential = (credential: Credential.OAuth) =>
+  credential.metadata?.client as OAuthClientInformationMixed | undefined
+
+/** Folds SDK tokens (plus DCR client info and the server URL) into a storable credential. */
+export const toCredential = (input: {
+  readonly methodID: Integration.MethodID
+  readonly serverUrl: string
+  readonly tokens: OAuthTokens
+  readonly client: OAuthClientInformationMixed | undefined
+}) =>
+  Credential.OAuth.make({
+    type: "oauth",
+    methodID: input.methodID,
+    access: input.tokens.access_token,
+    refresh: input.tokens.refresh_token ?? "",
+    // 0 marks an unknown/non-expiring token; toTokens then omits expires_in so the SDK won't force a refresh.
+    expires: input.tokens.expires_in ? Date.now() + input.tokens.expires_in * 1000 : 0,
+    metadata: {
+      serverUrl: input.serverUrl,
+      tokenType: input.tokens.token_type,
+      ...(input.tokens.scope ? { scope: input.tokens.scope } : {}),
+      ...(input.client ? { client: input.client } : {}),
+    },
+  })
+
+/** Reconstructs SDK tokens from a stored credential so the connect-time provider can present them. */
+export const toTokens = (credential: Credential.OAuth): OAuthTokens => {
+  const metadata = credential.metadata ?? {}
+  return {
+    access_token: credential.access,
+    token_type: typeof metadata.tokenType === "string" ? metadata.tokenType : "Bearer",
+    ...(credential.refresh ? { refresh_token: credential.refresh } : {}),
+    ...(credential.expires ? { expires_in: Math.max(0, Math.floor((credential.expires - Date.now()) / 1000)) } : {}),
+    ...(typeof metadata.scope === "string" ? { scope: metadata.scope } : {}),
+  }
+}
+
+/**
+ * Runs the interactive OAuth login for one remote MCP server. Stands up a loopback callback server,
+ * lets the SDK drive DCR + PKCE to produce an authorization URL, and returns an attempt whose callback
+ * exchanges the redirect code for a storable credential. Scoped: the callback server closes with the scope.
+ */
+export const authorize = (input: {
+  readonly name: string
+  readonly config: typeof ConfigMCP.Remote.Type
+  readonly methodID: Integration.MethodID
+}) =>
+  Effect.gen(function* () {
+    const oauth = input.config.oauth || undefined
+    const store = memoryStore()
+    const code = yield* Deferred.make<string, Error>()
+    const redirectPath = oauth?.redirect_uri ? new URL(oauth.redirect_uri).pathname : "/callback"
+    const state = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url")
+
+    const server = createServer((request, response) => {
+      const url = new URL(request.url ?? "/", "http://127.0.0.1")
+      if (url.pathname !== redirectPath) {
+        response.writeHead(404).end("Not found")
+        return
+      }
+      const fail = (reason: string) => {
+        Effect.runFork(Deferred.fail(code, new Error(reason)))
+        response.writeHead(400, { "Content-Type": "text/html" }).end(OauthCallbackPage.error(reason, { provider: input.name }))
+      }
+      const error = url.searchParams.get("error_description") ?? url.searchParams.get("error")
+      if (error) return fail(error)
+      // Reject a redirect whose state does not match what we issued: this is the CSRF defense the
+      // state parameter exists for, so an attacker can't inject their own authorization code.
+      if (url.searchParams.get("state") !== state) return fail("OAuth state mismatch")
+      const value = url.searchParams.get("code")
+      if (!value) return fail("Missing authorization code")
+      Effect.runFork(Deferred.succeed(code, value))
+      response.writeHead(200, { "Content-Type": "text/html" }).end(OauthCallbackPage.success({ provider: input.name }))
+    })
+
+    // Bind the port the redirect will actually arrive on: an explicit callback_port wins, else the port
+    // pinned by redirect_uri, else an ephemeral port. Binding ephemerally while redirect_uri names a fixed
+    // port would send the browser somewhere nothing is listening, hanging the attempt until it expires.
+    const redirectPort = oauth?.redirect_uri ? Number(new URL(oauth.redirect_uri).port) || undefined : undefined
+    const port = yield* Effect.callback<number, Error>((resume) => {
+      server.once("error", (error) => resume(Effect.fail(error)))
+      server.listen(oauth?.callback_port ?? redirectPort ?? 0, "127.0.0.1", () => {
+        const address = server.address()
+        resume(
+          address && typeof address === "object"
+            ? Effect.succeed(address.port)
+            : Effect.fail(new Error("Could not determine MCP OAuth callback port")),
+        )
+      })
+    })
+    yield* Effect.addFinalizer(() => Effect.sync(() => server.close()))
+
+    let authorizationUrl: URL | undefined
+    const oauthProvider = provider({
+      redirectUrl: oauth?.redirect_uri ?? `http://127.0.0.1:${port}${redirectPath}`,
+      scope: oauth?.scope,
+      state,
+      client: oauth?.client_id ? { id: oauth.client_id, secret: oauth.client_secret } : undefined,
+      onRedirect: (url) => {
+        authorizationUrl = url
+      },
+      store,
+    })
+
+    const finalize = Effect.gen(function* () {
+      const tokens = yield* Effect.promise(() => store.tokens())
+      if (!tokens) return yield* Effect.fail(new Error(`MCP server "${input.name}" did not return OAuth tokens`))
+      const client = yield* Effect.promise(() => store.clientInformation())
+      return toCredential({ methodID: input.methodID, serverUrl: input.config.url, tokens, client })
+    })
+
+    const result = yield* Effect.tryPromise({
+      try: () => auth(oauthProvider, { serverUrl: input.config.url, scope: oauth?.scope }),
+      catch: (error) => (error instanceof Error ? error : new Error(String(error))),
+    })
+
+    // The provider may already hold valid tokens (e.g. a re-auth), in which case there is no browser step.
+    if (result === "AUTHORIZED") {
+      return { url: input.config.url, instructions: `Connected to ${input.name}.`, mode: "auto" as const, callback: finalize }
+    }
+    if (!authorizationUrl)
+      return yield* Effect.fail(new Error(`MCP server "${input.name}" did not provide an authorization URL`))
+
+    return {
+      url: authorizationUrl.toString(),
+      instructions: `Authorize ${input.name} in your browser. This window will close automatically.`,
+      mode: "auto" as const,
+      callback: Deferred.await(code).pipe(
+        Effect.flatMap((value) =>
+          Effect.tryPromise({
+            try: () => auth(oauthProvider, { serverUrl: input.config.url, authorizationCode: value, scope: oauth?.scope }),
+            catch: (error) => (error instanceof Error ? error : new Error(String(error))),
+          }),
+        ),
+        Effect.flatMap(() => finalize),
+      ),
+    }
+  })

+ 4 - 1
packages/core/src/session/runner/llm.ts

@@ -20,6 +20,7 @@ import { SystemContext } from "../../system-context/index"
 import { SystemContextRegistry } from "../../system-context/registry"
 import { SkillGuidance } from "../../skill/guidance"
 import { ReferenceGuidance } from "../../reference/guidance"
+import { McpGuidance } from "../../mcp/guidance"
 import { ToolRegistry } from "../../tool/registry"
 import { ToolOutputStore } from "../../tool-output-store"
 import { SessionContextEpoch } from "../context-epoch"
@@ -102,6 +103,7 @@ export const layer = Layer.effect(
     const systemContext = yield* SystemContextRegistry.Service
     const skillGuidance = yield* SkillGuidance.Service
     const referenceGuidance = yield* ReferenceGuidance.Service
+    const mcpGuidance = yield* McpGuidance.Service
     const snapshots = yield* Snapshot.Service
     const db = (yield* Database.Service).db
     const compaction = yield* SessionCompaction.Service
@@ -160,7 +162,7 @@ export const layer = Layer.effect(
       new TurnTransitionError({ _tag: "ContinueAfterOverflowCompaction", step })
 
     const loadSystemContext = (agent: AgentV2.Selection) =>
-      Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load()], {
+      Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load(), mcpGuidance.load(agent)], {
         concurrency: "unbounded",
       }).pipe(Effect.map(SystemContext.combine))
 
@@ -424,6 +426,7 @@ export const node = makeLocationNode({
     SystemContextRegistry.node,
     SkillGuidance.node,
     ReferenceGuidance.node,
+    McpGuidance.node,
     SessionCompaction.node,
     Snapshot.node,
     Database.node,

+ 101 - 0
packages/core/src/tool/mcp.ts

@@ -0,0 +1,101 @@
+export * as McpTool from "./mcp"
+
+import { createHash } from "node:crypto"
+import { ToolFailure } from "@opencode-ai/llm"
+import { McpEvent } from "@opencode-ai/schema/mcp-event"
+import { Effect, Exit, type JsonSchema, Layer, Scope, Semaphore, Stream } from "effect"
+import { makeLocationNode } from "../effect/app-node"
+import { EventV2 } from "../event"
+import { MCP } from "../mcp"
+import { Tool } from "./tool"
+import { Tools } from "./tools"
+import { ToolRegistry } from "./registry"
+
+const MAX_NAME_LENGTH = 64
+const HASH_LENGTH = 8
+
+const sanitize = (value: string) => value.replace(/[^A-Za-z0-9_-]/g, "_")
+
+// Deterministic short suffix used to keep overlong or colliding names unique and stable across restarts.
+const hashSuffix = (raw: string) => "_" + createHash("sha1").update(raw).digest("hex").slice(0, HASH_LENGTH)
+
+const fit = (base: string, raw: string) => base.slice(0, MAX_NAME_LENGTH - HASH_LENGTH - 1) + hashSuffix(raw)
+
+/**
+ * Registry/permission action name for an MCP tool: V1-compatible `<server>_<tool>` so existing deny
+ * rules keep working. Sanitized to a valid tool name, prefixed when it would not start with a letter,
+ * and hashed down when it would exceed the 64-char limit.
+ */
+export const name = (server: string, tool: string) => {
+  const joined = sanitize(server) + "_" + sanitize(tool)
+  const base = /^[A-Za-z]/.test(joined) ? joined : "mcp_" + joined
+  return base.length > MAX_NAME_LENGTH ? fit(base, `${server}\u0000${tool}`) : base
+}
+
+const toContent = (part: MCP.ToolResultContent): Tool.Content =>
+  part.type === "text" ? { type: "text", text: part.text } : { type: "file", data: part.data, mime: part.mimeType }
+
+const errorText = (content: ReadonlyArray<MCP.ToolResultContent>) =>
+  content
+    .flatMap((part) => (part.type === "text" ? [part.text] : []))
+    .join("\n")
+    .trim()
+
+export const layer = Layer.effectDiscard(
+  Effect.gen(function* () {
+    const mcp = yield* MCP.Service
+    const tools = yield* Tools.Service
+    const events = yield* EventV2.Service
+    const scope = yield* Scope.Scope
+    const lock = Semaphore.makeUnsafe(1)
+    let current: Scope.Closeable | undefined
+
+    const make = (server: MCP.ServerName, tool: MCP.Tool) =>
+      Tool.make({
+        description: tool.description ?? "",
+        jsonSchema: (tool.inputSchema as JsonSchema.JsonSchema | undefined) ?? { type: "object", properties: {} },
+        execute: (input) =>
+          Effect.gen(function* () {
+            const result = yield* mcp.callTool({ server, name: tool.name, args: (input ?? {}) as Record<string, unknown> }).pipe(
+              Effect.catchTags({
+                "MCP.NotFoundError": (error) => new ToolFailure({ message: `MCP server "${error.server}" is not available` }),
+                "MCP.ToolCallError": (error) => new ToolFailure({ message: error.message }),
+              }),
+            )
+            if (result.isError)
+              return yield* new ToolFailure({ message: errorText(result.content) || "MCP tool returned an error" })
+            return { structured: result.structured ?? {}, content: result.content.map(toContent) }
+          }),
+      })
+
+    // Register the current tool set under a fresh child scope, then close the previous one so the
+    // registry never has a gap where MCP tools disappear mid-swap.
+    const reconcile = lock.withPermit(
+      Effect.gen(function* () {
+        const used = new Set<string>()
+        const record: Record<string, Tool.AnyTool> = {}
+        for (const tool of yield* mcp.tools()) {
+          const initial = name(tool.server, tool.name)
+          const key = used.has(initial) ? fit(initial, `${tool.server}\u0000${tool.name}`) : initial
+          used.add(key)
+          record[key] = make(tool.server, tool)
+        }
+        const next = yield* Scope.fork(scope)
+        yield* tools.register(record).pipe(Scope.provide(next), Effect.orDie)
+        if (current) yield* Scope.close(current, Exit.void)
+        current = next
+      }),
+    )
+
+    yield* reconcile.pipe(Effect.forkScoped)
+    yield* events
+      .subscribe(McpEvent.ToolsChanged)
+      .pipe(Stream.runForEach(() => reconcile), Effect.forkScoped({ startImmediately: true }))
+  }),
+)
+
+export const node = makeLocationNode({
+  name: "mcp-tools",
+  layer,
+  deps: [ToolRegistry.toolsNode, MCP.node, EventV2.node],
+})

+ 59 - 10
packages/core/src/tool/tool.ts

@@ -60,6 +60,23 @@ type Config<
   }) => ReadonlyArray<Content>
 }
 
+export type DynamicOutput = {
+  readonly structured: unknown
+  readonly content: ReadonlyArray<Content>
+}
+
+/**
+ * Config for a tool whose input shape is a raw JSON Schema not known at compile
+ * time (MCP servers, plugin manifests). Input is passed through as `unknown`;
+ * `execute` returns the already-projected structured value and model content.
+ */
+type DynamicConfig = {
+  readonly description: string
+  readonly jsonSchema: JsonSchema.JsonSchema
+  readonly outputSchema?: JsonSchema.JsonSchema
+  readonly execute: (input: unknown, context: Context) => Effect.Effect<DynamicOutput, ToolFailure>
+}
+
 type Runtime = {
   readonly permission?: string
   readonly definition: (name: string) => ToolDefinition
@@ -72,6 +89,17 @@ export function make<
   Input extends SchemaType<any>,
   Output extends SchemaType<any>,
   Structured extends SchemaType<any> = Output,
+>(config: Config<Input, Output, Structured>): Definition<Input, Structured>
+export function make(config: DynamicConfig): AnyTool
+export function make(config: Config<any, any, any> | DynamicConfig): AnyTool {
+  if ("jsonSchema" in config) return makeDynamic(config)
+  return makeTyped(config)
+}
+
+function makeTyped<
+  Input extends SchemaType<any>,
+  Output extends SchemaType<any>,
+  Structured extends SchemaType<any> = Output,
 >(config: Config<Input, Output, Structured>): Definition<Input, Structured> {
   const tool = Object.freeze({}) as Definition<Input, Structured>
   const definitions = new Map<string, ToolDefinition>()
@@ -113,16 +141,8 @@ export function make<
             Effect.map(({ output, structured }) => ({
               structured,
               content:
-                config.toModelOutput?.({ input, output }).map((part) =>
-                  part.type === "text"
-                    ? { type: "text" as const, text: part.text }
-                    : {
-                        type: "file" as const,
-                        uri: `data:${part.mime};base64,${part.data}`,
-                        mime: part.mime,
-                        name: part.name,
-                      },
-                ) ?? (typeof output === "string" ? [{ type: "text" as const, text: output }] : []),
+                config.toModelOutput?.({ input, output }).map(toModelContent) ??
+                (typeof output === "string" ? [{ type: "text" as const, text: output }] : []),
             })),
           ),
         ),
@@ -131,6 +151,35 @@ export function make<
   return tool
 }
 
+function makeDynamic(config: DynamicConfig): AnyTool {
+  const tool = Object.freeze({}) as AnyTool
+  const definitions = new Map<string, ToolDefinition>()
+  runtimes.set(tool, {
+    definition: (name) => {
+      const cached = definitions.get(name)
+      if (cached) return cached
+      const definition = new ToolDefinition({
+        name,
+        description: config.description,
+        inputSchema: config.jsonSchema,
+        outputSchema: config.outputSchema,
+      })
+      definitions.set(name, definition)
+      return definition
+    },
+    settle: (call, context) =>
+      config
+        .execute(call.input, context)
+        .pipe(Effect.map((output) => ({ structured: output.structured, content: output.content.map(toModelContent) }))),
+  })
+  return tool
+}
+
+function toModelContent(part: Content) {
+  if (part.type === "text") return { type: "text" as const, text: part.text }
+  return { type: "file" as const, uri: `data:${part.mime};base64,${part.data}`, mime: part.mime, name: part.name }
+}
+
 export const validateName = (name: string) =>
   /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name)
     ? Effect.void

+ 13 - 1
packages/core/src/v1/config/migrate.ts

@@ -29,7 +29,19 @@ const keys = new Set([
 
 export function isV1(input: unknown) {
   if (typeof input !== "object" || input === null || Array.isArray(input)) return false
-  return Object.keys(input).some((key) => keys.has(key))
+  const record = input as Record<string, unknown>
+  if (Object.keys(record).some((key) => keys.has(key))) return true
+  // `mcp` exists in both versions, so presence alone is ambiguous: v1 lists servers directly under
+  // `mcp`, while v2 nests them under `mcp.servers`. Only the v1 shape (a server entry with `type`)
+  // counts, so a bare `mcp`-only file still migrates instead of silently parsing to zero servers.
+  const mcp = record.mcp
+  return (
+    typeof mcp === "object" &&
+    mcp !== null &&
+    !Array.isArray(mcp) &&
+    !("servers" in mcp) &&
+    Object.values(mcp).some((server) => typeof server === "object" && server !== null && "type" in server)
+  )
 }
 
 export function migrate(info: typeof ConfigV1.Info.Type) {

+ 12 - 0
packages/core/test/config/config.test.ts

@@ -76,6 +76,18 @@ describe("Config", () => {
     }),
   )
 
+  it.effect("detects a bare v1-shaped mcp block while leaving v2 mcp config alone", () =>
+    Effect.sync(() => {
+      // V1 lists servers directly under `mcp`, so a file with only `$schema` + `mcp` still migrates.
+      expect(ConfigMigrateV1.isV1({ mcp: { context7: { type: "local", command: ["npx"] } } })).toBe(true)
+      expect(ConfigMigrateV1.isV1({ $schema: "x", mcp: { executor: { type: "remote", url: "https://x" } } })).toBe(true)
+      // V2 nests under `mcp.servers`, so it must not be misdetected and re-migrated.
+      expect(ConfigMigrateV1.isV1({ mcp: { servers: { context7: { type: "local", command: ["npx"] } } } })).toBe(false)
+      expect(ConfigMigrateV1.isV1({ mcp: {} })).toBe(false)
+      expect(ConfigMigrateV1.isV1({ mcp: { timeout: { request: 1000 } } })).toBe(false)
+    }),
+  )
+
   it.effect("migrates arbitrary v1 configuration into valid v2 configuration", () =>
     Effect.sync(() => {
       FastCheck.assert(

+ 3 - 0
packages/core/test/session-runner-recorded.test.ts

@@ -30,6 +30,7 @@ import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry
 import { SystemContext } from "@opencode-ai/core/system-context"
 import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
 import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
+import { McpGuidance } from "@opencode-ai/core/mcp/guidance"
 import { describe, expect } from "bun:test"
 import { eq } from "drizzle-orm"
 import { Effect, Layer } from "effect"
@@ -72,6 +73,7 @@ const systemContext = SystemContextRegistry.layer
 const location = Location.layer({ directory: AbsolutePath.make("/project") }).pipe(Layer.provide(Project.defaultLayer))
 const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
 const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
+const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
 const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
 const runner = SessionRunnerLLM.defaultLayer.pipe(
   Layer.provide(SessionCompaction.layer),
@@ -87,6 +89,7 @@ const runner = SessionRunnerLLM.defaultLayer.pipe(
   Layer.provide(agents),
   Layer.provide(skillGuidance),
   Layer.provide(referenceGuidance),
+  Layer.provide(mcpGuidance),
   Layer.provide(config),
 )
 const execution = Layer.effect(

+ 3 - 0
packages/core/test/session-runner.test.ts

@@ -52,6 +52,7 @@ import { SystemContext } from "@opencode-ai/core/system-context"
 import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry"
 import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
 import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
+import { McpGuidance } from "@opencode-ai/core/mcp/guidance"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { Location } from "@opencode-ai/core/location"
 import { ProviderV2 } from "@opencode-ai/core/provider"
@@ -217,6 +218,7 @@ const skillGuidance = Layer.mock(SkillGuidance.Service, {
     ),
 })
 const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
+const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
 const config = Layer.succeed(
   Config.Service,
   Config.Service.of({
@@ -248,6 +250,7 @@ const runner = SessionRunnerLLM.layer.pipe(
   Layer.provide(agents),
   Layer.provide(skillGuidance),
   Layer.provide(referenceGuidance),
+  Layer.provide(mcpGuidance),
   Layer.provide(config),
 )
 const execution = Layer.effect(

+ 2 - 0
packages/protocol/src/api.ts

@@ -21,6 +21,7 @@ import { ReferenceGroup } from "./groups/reference"
 import { Authorization } from "./middleware/authorization"
 import { LocationGroup } from "./groups/location"
 import { IntegrationGroup } from "./groups/integration"
+import { McpGroup } from "./groups/mcp"
 import { CredentialGroup } from "./groups/credential"
 import { ProjectGroup } from "./groups/project"
 import { ProjectCopyGroup } from "./groups/project-copy"
@@ -47,6 +48,7 @@ const makeApiFromGroup = <
     .add(GenerateGroup.middleware(locationMiddleware))
     .add(ProviderGroup.middleware(locationMiddleware))
     .add(IntegrationGroup.middleware(locationMiddleware))
+    .add(McpGroup.middleware(locationMiddleware))
     .add(CredentialGroup.middleware(locationMiddleware))
     .add(ProjectGroup.middleware(locationMiddleware))
     .add(makePermissionGroup(locationMiddleware, sessionLocationMiddleware))

+ 22 - 0
packages/protocol/src/groups/mcp.ts

@@ -0,0 +1,22 @@
+import { Mcp } from "@opencode-ai/schema/mcp"
+import { Location } from "@opencode-ai/schema/location"
+import { Schema } from "effect"
+import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { LocationQuery, locationQueryOpenApi } from "./location"
+
+export const McpGroup = HttpApiGroup.make("server.mcp")
+  .add(
+    HttpApiEndpoint.get("mcp.list", "/api/mcp", {
+      query: LocationQuery,
+      success: Location.response(Schema.Array(Mcp.Server)),
+    })
+      .annotateMerge(locationQueryOpenApi)
+      .annotateMerge(
+        OpenApi.annotations({
+          identifier: "v2.mcp.list",
+          summary: "List MCP servers",
+          description: "Retrieve configured MCP servers and their connection status.",
+        }),
+      ),
+  )
+  .annotateMerge(OpenApi.annotations({ title: "mcp", description: "MCP server status routes." }))

+ 10 - 1
packages/schema/src/mcp-event.ts

@@ -18,4 +18,13 @@ export const BrowserOpenFailed = Event.define({
   },
 })
 
-export const Definitions = Event.inventory(ToolsChanged, BrowserOpenFailed)
+// Emitted whenever a server's connection status settles (connected, failed, needs_auth, closed) so
+// observers can refresh status without polling.
+export const StatusChanged = Event.define({
+  type: "mcp.status.changed",
+  schema: {
+    server: Schema.String,
+  },
+})
+
+export const Definitions = Event.inventory(ToolsChanged, BrowserOpenFailed, StatusChanged)

+ 39 - 0
packages/schema/src/mcp.ts

@@ -0,0 +1,39 @@
+export * as Mcp from "./mcp"
+
+import { Schema } from "effect"
+
+const Connected = Schema.Struct({ status: Schema.Literal("connected") }).annotate({
+  identifier: "Mcp.Status.Connected",
+})
+const Disconnected = Schema.Struct({ status: Schema.Literal("disconnected") }).annotate({
+  identifier: "Mcp.Status.Disconnected",
+})
+const Disabled = Schema.Struct({ status: Schema.Literal("disabled") }).annotate({
+  identifier: "Mcp.Status.Disabled",
+})
+const Failed = Schema.Struct({ status: Schema.Literal("failed"), error: Schema.String }).annotate({
+  identifier: "Mcp.Status.Failed",
+})
+const NeedsAuth = Schema.Struct({ status: Schema.Literal("needs_auth") }).annotate({
+  identifier: "Mcp.Status.NeedsAuth",
+})
+const NeedsClientRegistration = Schema.Struct({
+  status: Schema.Literal("needs_client_registration"),
+  error: Schema.String,
+}).annotate({ identifier: "Mcp.Status.NeedsClientRegistration" })
+
+export type Status = typeof Status.Type
+export const Status = Schema.Union([
+  Connected,
+  Disconnected,
+  Disabled,
+  Failed,
+  NeedsAuth,
+  NeedsClientRegistration,
+]).pipe(Schema.toTaggedUnion("status"))
+
+export interface Server extends Schema.Schema.Type<typeof Server> {}
+export const Server = Schema.Struct({
+  name: Schema.String,
+  status: Status,
+}).annotate({ identifier: "Mcp.Server" })

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

@@ -299,6 +299,8 @@ import type {
   V2IntegrationListResponses,
   V2LocationGetErrors,
   V2LocationGetResponses,
+  V2McpListErrors,
+  V2McpListResponses,
   V2ModelListErrors,
   V2ModelListResponses,
   V2PermissionRequestListErrors,
@@ -6412,6 +6414,30 @@ export class Integration extends HeyApiClient {
   }
 }
 
+export class Mcp2 extends HeyApiClient {
+  /**
+   * List MCP servers
+   *
+   * Retrieve configured MCP servers and their connection status.
+   */
+  public list<ThrowOnError extends boolean = false>(
+    parameters?: {
+      location?: {
+        directory?: string
+        workspace?: string
+      }
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }])
+    return (options?.client ?? this.client).get<V2McpListResponses, V2McpListErrors, ThrowOnError>({
+      url: "/api/mcp",
+      ...options,
+      ...params,
+    })
+  }
+}
+
 export class Credential extends HeyApiClient {
   /**
    * Remove credential
@@ -7435,6 +7461,11 @@ export class V2 extends HeyApiClient {
     return (this._integration ??= new Integration({ client: this.client }))
   }
 
+  private _mcp?: Mcp2
+  get mcp(): Mcp2 {
+    return (this._mcp ??= new Mcp2({ client: this.client }))
+  }
+
   private _credential?: Credential
   get credential(): Credential {
     return (this._credential ??= new Credential({ client: this.client }))

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

@@ -84,6 +84,7 @@ export type Event =
   | EventTuiSessionSelect2
   | EventMcpToolsChanged
   | EventMcpBrowserOpenFailed
+  | EventMcpStatusChanged
   | EventCommandExecuted
   | EventProjectUpdated
   | EventSessionStatus
@@ -1551,6 +1552,13 @@ export type GlobalEvent = {
           url: string
         }
       }
+    | {
+        id: string
+        type: "mcp.status.changed"
+        properties: {
+          server: string
+        }
+      }
     | {
         id: string
         type: "command.executed"
@@ -3048,6 +3056,7 @@ export type V2Event =
   | TuiSessionSelect
   | McpToolsChanged
   | McpBrowserOpenFailed
+  | McpStatusChanged
   | CommandExecuted
   | ProjectUpdated
   | SessionStatus2
@@ -5254,6 +5263,43 @@ export type IntegrationAttemptStatus =
       }
     }
 
+export type McpStatusConnected2 = {
+  status: "connected"
+}
+
+export type McpStatusDisconnected = {
+  status: "disconnected"
+}
+
+export type McpStatusDisabled2 = {
+  status: "disabled"
+}
+
+export type McpStatusFailed2 = {
+  status: "failed"
+  error: string
+}
+
+export type McpStatusNeedsAuth2 = {
+  status: "needs_auth"
+}
+
+export type McpStatusNeedsClientRegistration2 = {
+  status: "needs_client_registration"
+  error: string
+}
+
+export type McpServer = {
+  name: string
+  status:
+    | McpStatusConnected2
+    | McpStatusDisconnected
+    | McpStatusDisabled2
+    | McpStatusFailed2
+    | McpStatusNeedsAuth2
+    | McpStatusNeedsClientRegistration2
+}
+
 export type ProjectCurrent = {
   id: string
   directory: string
@@ -6215,6 +6261,23 @@ export type McpBrowserOpenFailed = {
   }
 }
 
+export type McpStatusChanged = {
+  id: string
+  metadata?: {
+    [key: string]: unknown
+  }
+  type: "mcp.status.changed"
+  durable?: {
+    aggregateID: string
+    seq: number
+    version: number
+  }
+  location?: LocationRef
+  data: {
+    server: string
+  }
+}
+
 export type CommandExecuted = {
   id: string
   metadata?: {
@@ -7318,6 +7381,14 @@ export type EventMcpBrowserOpenFailed = {
   }
 }
 
+export type EventMcpStatusChanged = {
+  id: string
+  type: "mcp.status.changed"
+  properties: {
+    server: string
+  }
+}
+
 export type EventCommandExecuted = {
   id: string
   type: "command.executed"
@@ -13030,6 +13101,43 @@ export type V2IntegrationAttemptCompleteResponses = {
 export type V2IntegrationAttemptCompleteResponse =
   V2IntegrationAttemptCompleteResponses[keyof V2IntegrationAttemptCompleteResponses]
 
+export type V2McpListData = {
+  body?: never
+  path?: never
+  query?: {
+    location?: {
+      directory?: string
+      workspace?: string
+    }
+  }
+  url: "/api/mcp"
+}
+
+export type V2McpListErrors = {
+  /**
+   * InvalidRequestError
+   */
+  400: InvalidRequestError
+  /**
+   * UnauthorizedError
+   */
+  401: UnauthorizedError
+}
+
+export type V2McpListError = V2McpListErrors[keyof V2McpListErrors]
+
+export type V2McpListResponses = {
+  /**
+   * Success
+   */
+  200: {
+    location: LocationInfo
+    data: Array<McpServer>
+  }
+}
+
+export type V2McpListResponse = V2McpListResponses[keyof V2McpListResponses]
+
 export type V2CredentialRemoveData = {
   body?: never
   path: {

+ 2 - 0
packages/server/src/handlers.ts

@@ -17,6 +17,7 @@ import { QuestionHandler } from "./handlers/question"
 import { ReferenceHandler } from "./handlers/reference"
 import { LocationHandler } from "./handlers/location"
 import { IntegrationHandler } from "./handlers/integration"
+import { McpHandler } from "./handlers/mcp"
 import { CredentialHandler } from "./handlers/credential"
 import { ProjectHandler } from "./handlers/project"
 import { ProjectCopyHandler } from "./handlers/project-copy"
@@ -31,6 +32,7 @@ export const handlers = Layer.mergeAll(
   GenerateHandler,
   ProviderHandler,
   IntegrationHandler,
+  McpHandler,
   CredentialHandler,
   ProjectHandler,
   PermissionHandler,

+ 19 - 0
packages/server/src/handlers/mcp.ts

@@ -0,0 +1,19 @@
+import { MCP } from "@opencode-ai/core/mcp/index"
+import { Effect } from "effect"
+import { HttpApiBuilder } from "effect/unstable/httpapi"
+import { Api } from "../api"
+import { response } from "../location"
+
+export const McpHandler = HttpApiBuilder.group(Api, "server.mcp", (handlers) =>
+  Effect.gen(function* () {
+    return handlers.handle(
+      "mcp.list",
+      Effect.fn(function* () {
+        const service = yield* MCP.Service
+        return yield* response(
+          service.servers().pipe(Effect.map((servers) => servers.map((info) => ({ name: info.name, status: info.status })))),
+        )
+      }),
+    )
+  }),
+)

+ 28 - 0
packages/tui/src/app.tsx

@@ -376,6 +376,34 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
   const attention = createTuiAttention({ renderer, config: tuiConfig, kv })
   const clipboard = useClipboard()
 
+  // Toast once when an MCP server enters a failed or needs-auth state so the user knows to act,
+  // without having to open the status panel. Tracking the last alerted status avoids re-toasting
+  // the same problem on every refresh while still re-alerting if the state changes.
+  const mcpAlerted: Record<string, string> = {}
+  createEffect(() => {
+    for (const server of data.location.mcp.list() ?? []) {
+      const status = server.status
+      if (status.status !== "failed" && status.status !== "needs_auth") {
+        delete mcpAlerted[server.name]
+        continue
+      }
+      if (mcpAlerted[server.name] === status.status) continue
+      mcpAlerted[server.name] = status.status
+      if (status.status === "needs_auth")
+        toast.show({
+          variant: "warning",
+          title: "MCP server needs authentication",
+          message: `Connect "${server.name}" to use its tools.`,
+        })
+      else
+        toast.show({
+          variant: "error",
+          title: "MCP server failed to connect",
+          message: `${server.name}: ${status.error}`,
+        })
+    }
+  })
+
   const api = createTuiApi(
     createTuiApiAdapters({
       version: InstallationVersion,

+ 31 - 62
packages/tui/src/component/dialog-mcp.tsx

@@ -1,84 +1,53 @@
 import { createMemo, createSignal } from "solid-js"
-import { useLocal } from "../context/local"
-import { useSync } from "../context/sync"
-import { map, pipe, entries, sortBy } from "remeda"
-import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "../ui/dialog-select"
+import { useData } from "../context/data"
+import { map, pipe, sortBy } from "remeda"
+import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select"
 import { useTheme } from "../context/theme"
 import { TextAttributes } from "@opentui/core"
-import { useSDK } from "../context/sdk"
+import type { McpServer } from "@opencode-ai/sdk/v2"
 
-function Status(props: { enabled: boolean; loading: boolean }) {
+function Status(props: { status: McpServer["status"] }) {
   const { theme } = useTheme()
-  if (props.loading) {
-    return <span style={{ fg: theme.textMuted }}>⋯ Loading</span>
+  switch (props.status.status) {
+    case "connected":
+      return <span style={{ fg: theme.success, attributes: TextAttributes.BOLD }}>✓ Connected</span>
+    case "failed":
+      return <span style={{ fg: theme.error }}>✗ {props.status.error}</span>
+    case "needs_auth":
+      return <span style={{ fg: theme.warning }}>! Needs authentication</span>
+    case "needs_client_registration":
+      return <span style={{ fg: theme.error }}>✗ {props.status.error}</span>
+    case "disabled":
+      return <span style={{ fg: theme.textMuted }}>○ Disabled</span>
+    default:
+      return <span style={{ fg: theme.textMuted }}>○ Disconnected</span>
   }
-  if (props.enabled) {
-    return <span style={{ fg: theme.success, attributes: TextAttributes.BOLD }}>✓ Enabled</span>
-  }
-  return <span style={{ fg: theme.textMuted }}>○ Disabled</span>
 }
 
 export function DialogMcp() {
-  const local = useLocal()
-  const sync = useSync()
-  const sdk = useSDK()
+  const data = useData()
   const [, setRef] = createSignal<DialogSelectRef<unknown>>()
-  const [loading, setLoading] = createSignal<string | null>(null)
-
-  const options = createMemo(() => {
-    // Track sync data and loading state to trigger re-render when they change
-    const mcpData = sync.data.mcp
-    const loadingMcp = loading()
 
-    return pipe(
-      mcpData ?? {},
-      entries(),
-      sortBy(([name]) => name),
-      map(([name, status]) => ({
-        value: name,
-        title: name,
-        description: status.status === "failed" ? "failed" : status.status,
-        footer: <Status enabled={local.mcp.isEnabled(name)} loading={loadingMcp === name} />,
+  const options = createMemo(() =>
+    pipe(
+      data.location.mcp.list() ?? [],
+      sortBy((server) => server.name),
+      map((server) => ({
+        value: server.name,
+        title: server.name,
+        footer: <Status status={server.status} />,
         category: undefined,
       })),
-    )
-  })
-
-  const actions = createMemo(() => [
-    {
-      command: "dialog.mcp.toggle",
-      title: "toggle",
-      onTrigger: async (option: DialogSelectOption<string>) => {
-        // Prevent toggling while an operation is already in progress
-        if (loading() !== null) return
-
-        setLoading(option.value)
-        try {
-          await local.mcp.toggle(option.value)
-          // Refresh MCP status from server
-          const status = await sdk.client.mcp.status()
-          if (status.data) {
-            sync.set("mcp", status.data)
-          } else {
-            console.error("Failed to refresh MCP status: no data returned")
-          }
-        } catch (error) {
-          console.error("Failed to toggle MCP:", error)
-        } finally {
-          setLoading(null)
-        }
-      },
-    },
-  ])
+    ),
+  )
 
   return (
     <DialogSelect
       ref={setRef}
       title="MCPs"
       options={options()}
-      actions={actions()}
-      onSelect={(_option) => {
-        // Don't close on select, only on escape
+      onSelect={() => {
+        // Read-only view: selection does nothing, the dialog closes on escape.
       }}
     />
   )

+ 15 - 14
packages/tui/src/component/dialog-status.tsx

@@ -3,15 +3,18 @@ import { fileURLToPath } from "bun"
 import { useTheme } from "../context/theme"
 import { useDialog } from "../ui/dialog"
 import { useSync } from "../context/sync"
+import { useData } from "../context/data"
 import { For, Match, Switch, Show, createMemo } from "solid-js"
 
 export type DialogStatusProps = {}
 
 export function DialogStatus() {
   const sync = useSync()
+  const data = useData()
   const { theme } = useTheme()
   const dialog = useDialog()
 
+  const mcp = createMemo(() => data.location.mcp.list() ?? [])
   const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
 
   const plugins = createMemo(() => {
@@ -50,11 +53,11 @@ export function DialogStatus() {
           esc
         </text>
       </box>
-      <Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
+      <Show when={mcp().length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
         <box>
-          <text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
-          <For each={Object.entries(sync.data.mcp)}>
-            {([key, item]) => (
+          <text fg={theme.text}>{mcp().length} MCP Servers</text>
+          <For each={mcp()}>
+            {(item) => (
               <box flexDirection="row" gap={1}>
                 <text
                   flexShrink={0}
@@ -67,22 +70,20 @@ export function DialogStatus() {
                         needs_auth: theme.warning,
                         needs_client_registration: theme.error,
                       } as Record<string, typeof theme.success>
-                    )[item.status],
+                    )[item.status.status],
                   }}
                 >
                 </text>
                 <text fg={theme.text} wrapMode="word">
-                  <b>{key}</b>{" "}
+                  <b>{item.name}</b>{" "}
                   <span style={{ fg: theme.textMuted }}>
-                    <Switch fallback={item.status}>
-                      <Match when={item.status === "connected"}>Connected</Match>
-                      <Match when={item.status === "failed" && item}>{(val) => val().error}</Match>
-                      <Match when={item.status === "disabled"}>Disabled in configuration</Match>
-                      <Match when={(item.status as string) === "needs_auth"}>
-                        Needs authentication (run: opencode mcp auth {key})
-                      </Match>
-                      <Match when={(item.status as string) === "needs_client_registration" && item}>
+                    <Switch fallback={item.status.status}>
+                      <Match when={item.status.status === "connected"}>Connected</Match>
+                      <Match when={item.status.status === "failed" && item.status}>{(val) => val().error}</Match>
+                      <Match when={item.status.status === "disabled"}>Disabled in configuration</Match>
+                      <Match when={item.status.status === "needs_auth"}>Needs authentication</Match>
+                      <Match when={item.status.status === "needs_client_registration" && item.status}>
                         {(val) => (val() as { error: string }).error}
                       </Match>
                     </Switch>

+ 18 - 0
packages/tui/src/context/data.tsx

@@ -3,6 +3,7 @@ import type {
   CommandV2Info,
   IntegrationInfo,
   LocationRef,
+  McpServer,
   ModelV2Info,
   PermissionSavedInfo,
   PermissionV2Request,
@@ -30,6 +31,7 @@ type LocationData = {
   agent?: AgentV2Info[]
   command?: CommandV2Info[]
   integration?: IntegrationInfo[]
+  mcp?: McpServer[]
   model?: ModelV2Info[]
   provider?: ProviderV2Info[]
   reference?: ReferenceInfo[]
@@ -529,6 +531,11 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
             result.location.provider.refresh(event.location),
           ])
           break
+        // Authenticating an MCP integration reconnects its server, which emits mcp.status.changed,
+        // so the mcp list refreshes here rather than off integration.updated.
+        case "mcp.status.changed":
+          void result.location.mcp.refresh(event.location)
+          break
       }
     }
 
@@ -674,6 +681,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
             setStore("location", key, { ...store.location[key], integration: mutable(result.data) })
           },
         },
+        mcp: {
+          list(location?: LocationRef) {
+            return store.location[locationKey(location ?? defaultLocation())]?.mcp
+          },
+          async refresh(ref?: LocationRef) {
+            const result = await sdk.client.v2.mcp.list({ location: locationQuery(ref) }, { throwOnError: true })
+            const key = locationKey(result.data.location)
+            setStore("location", key, { ...store.location[key], mcp: result.data.data })
+          },
+        },
         model: {
           list(location?: LocationRef) {
             return store.location[locationKey(location ?? defaultLocation())]?.model
@@ -747,6 +764,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
         result.location.refresh(),
         result.location.agent.refresh(),
         result.location.integration.refresh(),
+        result.location.mcp.refresh(),
         result.location.model.refresh(),
         result.location.provider.refresh(),
         result.location.reference.refresh(),

+ 4 - 2
packages/tui/src/routes/session/footer.tsx

@@ -1,6 +1,7 @@
 import { createMemo, Match, onCleanup, onMount, Show, Switch } from "solid-js"
 import { useTheme } from "../../context/theme"
 import { useSync } from "../../context/sync"
+import { useData } from "../../context/data"
 import { useDirectory } from "../../context/directory"
 import { useConnected } from "../../component/use-connected"
 import { createStore } from "solid-js/store"
@@ -9,9 +10,10 @@ import { useRoute } from "../../context/route"
 export function Footer() {
   const { theme } = useTheme()
   const sync = useSync()
+  const data = useData()
   const route = useRoute()
-  const mcp = createMemo(() => Object.values(sync.data.mcp).filter((x) => x.status === "connected").length)
-  const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed"))
+  const mcp = createMemo(() => (data.location.mcp.list() ?? []).filter((x) => x.status.status === "connected").length)
+  const mcpError = createMemo(() => (data.location.mcp.list() ?? []).some((x) => x.status.status === "failed"))
   const lsp = createMemo(() => Object.keys(sync.data.lsp))
   const permissions = createMemo(() => {
     if (route.data.type !== "session") return []