diff --git a/bun.lock b/bun.lock index 48c6ce9af38..b652ae24168 100644 --- a/bun.lock +++ b/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=="], diff --git a/packages/client/src/generated-effect/client.ts b/packages/client/src/generated-effect/client.ts index 465a245d1a9..31df1aacc32 100644 --- a/packages/client/src/generated-effect/client.ts +++ b/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[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[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[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[0] -type Endpoint9_1Input = { - readonly credentialID: Endpoint9_1Request["params"]["credentialID"] - readonly location?: Endpoint9_1Request["query"]["location"] +type Endpoint10_1Request = Parameters[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[0] -type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] } -const Endpoint10_0 = (raw: RawClient["server.project"]) => (input?: Endpoint10_0Input) => +type Endpoint11_0Request = Parameters[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[0] -type Endpoint10_1Input = { - readonly projectID: Endpoint10_1Request["params"]["projectID"] - readonly location?: Endpoint10_1Request["query"]["location"] +type Endpoint11_1Request = Parameters[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[0] -type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } -const Endpoint11_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint11_0Input) => +type Endpoint12_0Request = Parameters[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[0] -type Endpoint11_1Input = { readonly projectID?: Endpoint11_1Request["query"]["projectID"] } -const Endpoint11_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint11_1Input) => +type Endpoint12_1Request = Parameters[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[0] -type Endpoint11_2Input = { readonly id: Endpoint11_2Request["params"]["id"] } -const Endpoint11_2 = (raw: RawClient["server.permission"]) => (input: Endpoint11_2Input) => +type Endpoint12_2Request = Parameters[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[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"] +type Endpoint12_3Request = Parameters[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 Endpoint11_3 = (raw: RawClient["server.permission"]) => (input: Endpoint11_3Input) => +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[0] -type Endpoint11_4Input = { readonly sessionID: Endpoint11_4Request["params"]["sessionID"] } -const Endpoint11_4 = (raw: RawClient["server.permission"]) => (input: Endpoint11_4Input) => +type Endpoint12_4Request = Parameters[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[0] -type Endpoint11_5Input = { - readonly sessionID: Endpoint11_5Request["params"]["sessionID"] - readonly requestID: Endpoint11_5Request["params"]["requestID"] +type Endpoint12_5Request = Parameters[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[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[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[0] -type Endpoint12_0Input = { - readonly location?: Endpoint12_0Request["query"]["location"] - readonly path?: Endpoint12_0Request["query"]["path"] +type Endpoint13_0Request = Parameters[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[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[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[0] -type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] } -const Endpoint13_0 = (raw: RawClient["server.command"]) => (input?: Endpoint13_0Input) => +type Endpoint14_0Request = Parameters[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[0] -type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } -const Endpoint14_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint14_0Input) => +type Endpoint15_0Request = Parameters[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[0] -type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] } -const Endpoint16_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint16_0Input) => +type Endpoint17_0Request = Parameters[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[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[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[0] -type Endpoint16_2Input = { - readonly ptyID: Endpoint16_2Request["params"]["ptyID"] - readonly location?: Endpoint16_2Request["query"]["location"] +type Endpoint17_2Request = Parameters[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[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[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[0] -type Endpoint16_4Input = { - readonly ptyID: Endpoint16_4Request["params"]["ptyID"] - readonly location?: Endpoint16_4Request["query"]["location"] +type Endpoint17_4Request = Parameters[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[0] -type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } -const Endpoint17_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint17_0Input) => +type Endpoint18_0Request = Parameters[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[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[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[0] -type Endpoint17_2Input = { - readonly id: Endpoint17_2Request["params"]["id"] - readonly location?: Endpoint17_2Request["query"]["location"] +type Endpoint18_2Request = Parameters[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[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[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[0] -type Endpoint17_4Input = { - readonly id: Endpoint17_4Request["params"]["id"] - readonly location?: Endpoint17_4Request["query"]["location"] +type Endpoint18_4Request = Parameters[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[0] -type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } -const Endpoint18_0 = (raw: RawClient["server.question"]) => (input?: Endpoint18_0Input) => +type Endpoint19_0Request = Parameters[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[0] -type Endpoint18_1Input = { readonly sessionID: Endpoint18_1Request["params"]["sessionID"] } -const Endpoint18_1 = (raw: RawClient["server.question"]) => (input: Endpoint18_1Input) => +type Endpoint19_1Request = Parameters[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[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[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[0] -type Endpoint18_3Input = { - readonly sessionID: Endpoint18_3Request["params"]["sessionID"] - readonly requestID: Endpoint18_3Request["params"]["requestID"] +type Endpoint19_3Request = Parameters[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[0] -type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] } -const Endpoint19_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint19_0Input) => +type Endpoint20_0Request = Parameters[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[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[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[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[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[0] -type Endpoint20_2Input = { - readonly projectID: Endpoint20_2Request["params"]["projectID"] - readonly location?: Endpoint20_2Request["query"]["location"] +type Endpoint21_2Request = Parameters[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 }) => diff --git a/packages/client/src/generated/client.ts b/packages/client/src/generated/client.ts index 7ea6525fae5..a04597f6b1c 100644 --- a/packages/client/src/generated/client.ts +++ b/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( + { + method: "GET", + path: `/api/mcp`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, credential: { update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) => request( diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index 79234985d70..72796c3c34a 100644 --- a/packages/client/src/generated/types.ts +++ b/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?: { diff --git a/packages/core/package.json b/packages/core/package.json index edd9827c486..be7fcc7006c 100644 --- a/packages/core/package.json +++ b/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", diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index cdc10ff3c81..81654eb3a48 100644 --- a/packages/core/src/location-services.ts +++ b/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, diff --git a/packages/core/src/mcp/client.ts b/packages/core/src/mcp/client.ts new file mode 100644 index 00000000000..4a9b17f6b16 --- /dev/null +++ b/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()("MCP.NeedsAuthError", { + server: Schema.String, +}) {} + +export class ConnectError extends Schema.TaggedErrorClass()("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 +} + +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 + /** Invokes a tool on the server. Interruption aborts the in-flight request. */ + readonly callTool: (input: { + readonly name: string + readonly args?: Record + }) => Effect.Effect + 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), + ...(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((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( + list: (cursor: string | undefined) => Promise, + items: (result: R) => T[], +) { + const collected: T[] = [] + const seen = new Set() + 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, + ) diff --git a/packages/core/src/mcp/guidance.ts b/packages/core/src/mcp/guidance.ts new file mode 100644 index 00000000000..7aa73f63dfb --- /dev/null +++ b/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) => + [ + "", + ...servers.flatMap((server) => [ + ` `, + ...server.instructions.split("\n").map((line) => ` ${line}`), + " ", + ]), + "", + ].join("\n") + +export interface Interface { + readonly load: (agent: AgentV2.Selection) => Effect.Effect +} + +export class Service extends Context.Service()("@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] }) diff --git a/packages/core/src/mcp/index.ts b/packages/core/src/mcp/index.ts new file mode 100644 index 00000000000..3a422470fe0 --- /dev/null +++ b/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("MCP.ServerInfo")({ + name: ServerName, + status: Status, + integrationID: Integration.ID.pipe(Schema.optional), + connection: IntegrationConnection.Info.pipe(Schema.optional), +}) {} + +export class ServerInstructions extends Schema.Class("MCP.ServerInstructions")({ + server: ServerName, + instructions: Schema.String, +}) {} + +export class Tool extends Schema.Class("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("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("MCP.PromptArgument")({ + name: Schema.String, + description: Schema.String.pipe(Schema.optional), + required: Schema.Boolean.pipe(Schema.optional), +}) {} + +export class Prompt extends Schema.Class("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("MCP.PromptMessage")({ + role: Schema.String, + content: Schema.Unknown, +}) {} + +export class PromptResult extends Schema.Class("MCP.PromptResult")({ + server: ServerName, + name: Schema.String, + messages: Schema.Array(PromptMessage), +}) {} + +export class Resource extends Schema.Class("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("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("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("MCP.ResourceContent")({ + server: ServerName, + uri: Schema.String, + contents: Schema.Array(ResourceContentPart), +}) {} + +export class NotFoundError extends Schema.TaggedErrorClass()("MCP.NotFoundError", { + server: ServerName, +}) {} + +export class ToolCallError extends Schema.TaggedErrorClass()("MCP.ToolCallError", { + server: ServerName, + tool: Schema.String, + message: Schema.String, +}) {} + +type ServerEntry = { + readonly config: typeof ConfigMCP.Server.Type + status: Status + readonly startup: Deferred.Deferred + scope?: Scope.Closeable + client?: MCPClient.Connection + tools?: ReadonlyArray + // 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 + readonly tools: () => Effect.Effect + readonly callTool: (input: { + readonly server: ServerName | string + readonly name: string + readonly args?: Record + }) => Effect.Effect + readonly instructions: () => Effect.Effect + readonly prompts: () => Effect.Effect + readonly prompt: (input: { + readonly server: ServerName | string + readonly name: string + readonly args?: Record + }) => Effect.Effect + readonly resourceCatalog: () => Effect.Effect + readonly readResource: (input: { + readonly server: ServerName | string + readonly uri: string + }) => Effect.Effect +} + +export class Service extends Context.Service()("@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() + 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() + 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(), + }) + } + } + + // 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], +}) diff --git a/packages/core/src/mcp/oauth.ts b/packages/core/src/mcp/oauth.ts new file mode 100644 index 00000000000..fc4752c7af8 --- /dev/null +++ b/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 + readonly saveTokens: (tokens: OAuthTokens) => Promise + readonly clientInformation: () => Promise + readonly saveClientInformation: (info: OAuthClientInformationMixed) => Promise + readonly codeVerifier: () => Promise + readonly saveCodeVerifier: (verifier: string) => Promise +} + +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 + /** Receives the authorization URL so the caller can open a browser and capture the eventual code. */ + readonly onRedirect: (url: URL) => void | Promise + 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() + 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((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), + ), + } + }) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 089991eed24..6d0e768dbc8 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/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, diff --git a/packages/core/src/tool/mcp.ts b/packages/core/src/tool/mcp.ts new file mode 100644 index 00000000000..571b747babe --- /dev/null +++ b/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 `_` 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) => + 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 }).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() + const record: Record = {} + 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], +}) diff --git a/packages/core/src/tool/tool.ts b/packages/core/src/tool/tool.ts index 8ee5b76596f..1968fc338bc 100644 --- a/packages/core/src/tool/tool.ts +++ b/packages/core/src/tool/tool.ts @@ -60,6 +60,23 @@ type Config< }) => ReadonlyArray } +export type DynamicOutput = { + readonly structured: unknown + readonly content: ReadonlyArray +} + +/** + * 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 +} + type Runtime = { readonly permission?: string readonly definition: (name: string) => ToolDefinition @@ -72,6 +89,17 @@ export function make< Input extends SchemaType, Output extends SchemaType, Structured extends SchemaType = Output, +>(config: Config): Definition +export function make(config: DynamicConfig): AnyTool +export function make(config: Config | DynamicConfig): AnyTool { + if ("jsonSchema" in config) return makeDynamic(config) + return makeTyped(config) +} + +function makeTyped< + Input extends SchemaType, + Output extends SchemaType, + Structured extends SchemaType = Output, >(config: Config): Definition { const tool = Object.freeze({}) as Definition const definitions = new Map() @@ -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() + 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 diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index 5f62858e3cb..2a9e1c73832 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/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 + 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) { diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 0f61c6c1df9..f16312b7634 100644 --- a/packages/core/test/config/config.test.ts +++ b/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( diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts index fd819f9f7a5..a6c86658b5d 100644 --- a/packages/core/test/session-runner-recorded.test.ts +++ b/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( diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index aa9dfd63f62..2e17444321d 100644 --- a/packages/core/test/session-runner.test.ts +++ b/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( diff --git a/packages/protocol/src/api.ts b/packages/protocol/src/api.ts index e879c99d7a1..03129d36186 100644 --- a/packages/protocol/src/api.ts +++ b/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)) diff --git a/packages/protocol/src/groups/mcp.ts b/packages/protocol/src/groups/mcp.ts new file mode 100644 index 00000000000..450a2a67c4c --- /dev/null +++ b/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." })) diff --git a/packages/schema/src/mcp-event.ts b/packages/schema/src/mcp-event.ts index 1d050df9273..753dea2ef75 100644 --- a/packages/schema/src/mcp-event.ts +++ b/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) diff --git a/packages/schema/src/mcp.ts b/packages/schema/src/mcp.ts new file mode 100644 index 00000000000..7c055a0ab2d --- /dev/null +++ b/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 {} +export const Server = Schema.Struct({ + name: Schema.String, + status: Status, +}).annotate({ identifier: "Mcp.Server" }) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index f567f48ed1c..3039b1f82d1 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/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( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + 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 })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 57487cf2f77..4788320adcb 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/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 + } +} + +export type V2McpListResponse = V2McpListResponses[keyof V2McpListResponses] + export type V2CredentialRemoveData = { body?: never path: { diff --git a/packages/server/src/handlers.ts b/packages/server/src/handlers.ts index b7269495fd1..acb16fb8e43 100644 --- a/packages/server/src/handlers.ts +++ b/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, diff --git a/packages/server/src/handlers/mcp.ts b/packages/server/src/handlers/mcp.ts new file mode 100644 index 00000000000..f488ed718ea --- /dev/null +++ b/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 })))), + ) + }), + ) + }), +) diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 820a069e839..86f0edcac37 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -376,6 +376,34 @@ function App(props: { onSnapshot?: () => Promise; 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 = {} + 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, diff --git a/packages/tui/src/component/dialog-mcp.tsx b/packages/tui/src/component/dialog-mcp.tsx index c48c0f8ee18..f5f82f22026 100644 --- a/packages/tui/src/component/dialog-mcp.tsx +++ b/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 ⋯ Loading + switch (props.status.status) { + case "connected": + return ✓ Connected + case "failed": + return ✗ {props.status.error} + case "needs_auth": + return ! Needs authentication + case "needs_client_registration": + return ✗ {props.status.error} + case "disabled": + return ○ Disabled + default: + return ○ Disconnected } - if (props.enabled) { - return ✓ Enabled - } - return ○ Disabled } export function DialogMcp() { - const local = useLocal() - const sync = useSync() - const sdk = useSDK() + const data = useData() const [, setRef] = createSignal>() - const [loading, setLoading] = createSignal(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: , + const options = createMemo(() => + pipe( + data.location.mcp.list() ?? [], + sortBy((server) => server.name), + map((server) => ({ + value: server.name, + title: server.name, + footer: , category: undefined, })), - ) - }) - - const actions = createMemo(() => [ - { - command: "dialog.mcp.toggle", - title: "toggle", - onTrigger: async (option: DialogSelectOption) => { - // 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 ( { - // Don't close on select, only on escape + onSelect={() => { + // Read-only view: selection does nothing, the dialog closes on escape. }} /> ) diff --git a/packages/tui/src/component/dialog-status.tsx b/packages/tui/src/component/dialog-status.tsx index 6c8fabdbb3a..1446dfc4ae2 100644 --- a/packages/tui/src/component/dialog-status.tsx +++ b/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 - 0} fallback={No MCP Servers}> + 0} fallback={No MCP Servers}> - {Object.keys(sync.data.mcp).length} MCP Servers - - {([key, item]) => ( + {mcp().length} MCP Servers + + {(item) => ( - )[item.status], + )[item.status.status], }} > • - {key}{" "} + {item.name}{" "} - - Connected - {(val) => val().error} - Disabled in configuration - - Needs authentication (run: opencode mcp auth {key}) - - + + Connected + {(val) => val().error} + Disabled in configuration + Needs authentication + {(val) => (val() as { error: string }).error} diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 12ba3a7f3aa..6c44d0fc013 100644 --- a/packages/tui/src/context/data.tsx +++ b/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(), diff --git a/packages/tui/src/routes/session/footer.tsx b/packages/tui/src/routes/session/footer.tsx index c3a96254e98..d163f21477c 100644 --- a/packages/tui/src/routes/session/footer.tsx +++ b/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 []