From 5cff8a4c3d56aec9e9cbea3cbfec70526ee0226f Mon Sep 17 00:00:00 2001 From: lewis617 Date: Thu, 28 May 2026 18:21:15 +0800 Subject: [PATCH 1/4] chore: upgrade @modelcontextprotocol/sdk from 1.18.2 to 1.29.0 Remove tools capability from ClientCapabilities as it is no longer a top-level field in the MCP SDK 1.29.0 schema. --- packages/agent-sdk/package.json | 2 +- packages/agent-sdk/src/managers/mcpManager.ts | 4 +- .../tests/managers/mcpManager.test.ts | 2 +- pnpm-lock.yaml | 150 +++++++++++++----- 4 files changed, 116 insertions(+), 42 deletions(-) diff --git a/packages/agent-sdk/package.json b/packages/agent-sdk/package.json index 149bb478..0c91241a 100644 --- a/packages/agent-sdk/package.json +++ b/packages/agent-sdk/package.json @@ -40,7 +40,7 @@ "prepublishOnly": "pnpm run build" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.2", + "@modelcontextprotocol/sdk": "^1.29.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.217.0", "@opentelemetry/exporter-logs-otlp-http": "^0.217.0", diff --git a/packages/agent-sdk/src/managers/mcpManager.ts b/packages/agent-sdk/src/managers/mcpManager.ts index 062135c4..3108b304 100644 --- a/packages/agent-sdk/src/managers/mcpManager.ts +++ b/packages/agent-sdk/src/managers/mcpManager.ts @@ -369,9 +369,7 @@ export class McpManager { version: "1.0.0", }, { - capabilities: { - tools: {}, - }, + capabilities: {}, }, ); diff --git a/packages/agent-sdk/tests/managers/mcpManager.test.ts b/packages/agent-sdk/tests/managers/mcpManager.test.ts index 94080cd6..8aa05c4d 100644 --- a/packages/agent-sdk/tests/managers/mcpManager.test.ts +++ b/packages/agent-sdk/tests/managers/mcpManager.test.ts @@ -539,7 +539,7 @@ describe("McpManager", () => { expect(result).toBe(true); expect(Client).toHaveBeenCalledWith( { name: "wave-code", version: "1.0.0" }, - { capabilities: { tools: {} } }, + { capabilities: {} }, ); expect(StdioClientTransport).toHaveBeenCalledWith({ command: "npx", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3f70b9d..159dfabd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,8 +51,8 @@ importers: packages/agent-sdk: dependencies: '@modelcontextprotocol/sdk': - specifier: ^1.18.2 - version: 1.19.1 + specifier: ^1.29.0 + version: 1.29.0(zod@3.25.76) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.1 @@ -643,6 +643,12 @@ packages: engines: {node: '>=6'} hasBin: true + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -687,9 +693,15 @@ packages: '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} - '@modelcontextprotocol/sdk@1.19.1': - resolution: {integrity: sha512-3Y2h3MZKjec1eAqSTBclATlX+AbC6n1LgfVzRMJLt3v6w0RCYgwLrjbxPDbhsYHt6Wdqc/aCceNJYgj448ELQQ==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -1202,9 +1214,20 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-escapes@7.2.0: resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} engines: {node: '>=18'} @@ -1296,8 +1319,8 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} boolbase@1.0.0: @@ -1710,14 +1733,14 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - express-rate-limit@7.5.1: - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} fast-deep-equal@3.1.3: @@ -1733,6 +1756,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1899,6 +1925,10 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + hono@4.12.23: + resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} + engines: {node: '>=16.9.0'} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -1923,10 +1953,6 @@ packages: engines: {node: '>=18'} hasBin: true - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - iconv-lite@0.7.0: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} @@ -1984,6 +2010,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2137,6 +2167,9 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -2162,6 +2195,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2520,6 +2559,10 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + queue-lit@1.5.2: resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} engines: {node: '>=12'} @@ -3177,10 +3220,10 @@ packages: yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} - zod-to-json-schema@3.24.6: - resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: - zod: ^3.24.1 + zod: ^3.25.28 || ^4 zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -3478,6 +3521,10 @@ snapshots: protobufjs: 7.5.7 yargs: 17.7.2 + '@hono/node-server@1.19.14(hono@4.12.23)': + dependencies: + hono: 4.12.23 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -3510,20 +3557,25 @@ snapshots: '@mixmark-io/domino@2.2.0': {} - '@modelcontextprotocol/sdk@1.19.1': + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: - ajv: 6.12.6 + '@hono/node-server': 1.19.14(hono@4.12.23) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 cors: 2.8.5 cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.0.6 - express: 5.1.0 - express-rate-limit: 7.5.1(express@5.1.0) + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.23 + jose: 6.2.3 + json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 raw-body: 3.0.1 zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) + zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: - supports-color @@ -4115,6 +4167,10 @@ snapshots: agent-base@7.1.4: optional: true + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -4122,6 +4178,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-escapes@7.2.0: dependencies: environment: 1.1.0 @@ -4235,15 +4298,15 @@ snapshots: binary-extensions@2.3.0: {} - body-parser@2.2.0: + 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.6.3 + iconv-lite: 0.7.0 on-finished: 2.4.1 - qs: 6.14.0 + qs: 6.15.2 raw-body: 3.0.1 type-is: 2.0.1 transitivePeerDependencies: @@ -4797,19 +4860,21 @@ snapshots: expect-type@1.3.0: {} - express-rate-limit@7.5.1(express@5.1.0): + express-rate-limit@8.5.2(express@5.2.1): dependencies: - express: 5.1.0 + express: 5.2.1 + ip-address: 10.2.0 - express@5.1.0: + express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.0 + body-parser: 2.2.2 content-disposition: 1.0.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.1 + debug: 4.4.3 + depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -4847,6 +4912,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.2: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -5013,6 +5080,8 @@ snapshots: highlight.js@11.11.1: {} + hono@4.12.23: {} + html-encoding-sniffer@6.0.0: dependencies: '@exodus/bytes': 1.11.0 @@ -5048,10 +5117,6 @@ snapshots: husky@9.1.7: {} - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.7.0: dependencies: safer-buffer: 2.1.2 @@ -5122,6 +5187,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ip-address@10.2.0: {} + ipaddr.js@1.9.1: {} is-array-buffer@3.0.5: @@ -5277,6 +5344,8 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + jose@6.2.3: {} + js-tokens@10.0.0: {} js-tokens@4.0.0: {} @@ -5316,6 +5385,10 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} jsx-ast-utils@3.3.5: @@ -5670,6 +5743,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + queue-lit@1.5.2: {} queue-microtask@1.2.3: {} @@ -5725,8 +5802,7 @@ snapshots: require-directory@2.1.1: {} - require-from-string@2.0.2: - optional: true + require-from-string@2.0.2: {} require-in-the-middle@8.0.1: dependencies: @@ -6397,7 +6473,7 @@ snapshots: yoga-layout@3.2.1: {} - zod-to-json-schema@3.24.6(zod@3.25.76): + zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76 From 3dc3b17bb14353731bc3d11fffc83d533cd74b3e Mon Sep 17 00:00:00 2001 From: lewis617 Date: Thu, 28 May 2026 18:33:24 +0800 Subject: [PATCH 2/4] feat: add type field to McpServerConfig for explicit transport selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add type?: 'stdio' | 'sse' | 'http' to McpServerConfig, replacing the implicit HTTP→SSE fallback with explicit type-based transport dispatch. URL-based servers without type default to Streamable HTTP (no fallback). ACP conversion preserves type; UI shows type in server detail view. --- packages/agent-sdk/src/managers/mcpManager.ts | 103 ++++++------ packages/agent-sdk/src/types/mcp.ts | 1 + .../tests/managers/mcpManager.test.ts | 146 +++++++++++++++--- packages/code/src/acp/agent.ts | 5 +- packages/code/src/components/McpManager.tsx | 7 + .../code/tests/components/McpManager.test.tsx | 24 +++ 6 files changed, 222 insertions(+), 64 deletions(-) diff --git a/packages/agent-sdk/src/managers/mcpManager.ts b/packages/agent-sdk/src/managers/mcpManager.ts index 3108b304..70537156 100644 --- a/packages/agent-sdk/src/managers/mcpManager.ts +++ b/packages/agent-sdk/src/managers/mcpManager.ts @@ -373,55 +373,65 @@ export class McpManager { }, ); - if (server.config.url) { + const serverType = server.config.type; + + if (serverType === "http" || (!serverType && server.config.url)) { + if (!server.config.url) { + throw new Error( + `MCP server ${name} with type "http" requires a 'url'`, + ); + } const url = new URL(server.config.url); const headers = server.config.headers; - - try { - logger?.debug( - `Attempting Streamable HTTP connection for ${name} at ${url.href}`, + logger?.debug( + `Connecting to MCP server ${name} using Streamable HTTP at ${url.href}`, + ); + transport = new StreamableHTTPClientTransport(url, { + requestInit: { headers }, + }); + client = createClient(); + await client.connect(transport); + const toolsResponse = await client.listTools(); + tools = + toolsResponse.tools?.map((tool) => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + })) || []; + logger?.info(`Connected to MCP server ${name} using Streamable HTTP`); + } else if (serverType === "sse") { + if (!server.config.url) { + throw new Error( + `MCP server ${name} with type "sse" requires a 'url'`, ); - const streamableTransport = new StreamableHTTPClientTransport(url, { - requestInit: { headers }, - }); - - const streamableClient = createClient(); - await streamableClient.connect(streamableTransport); - - // Try to list tools to verify connection works - const toolsResponse = await streamableClient.listTools(); - - transport = streamableTransport; - client = streamableClient; - tools = - toolsResponse.tools?.map((tool) => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - })) || []; - - logger?.info(`Connected to MCP server ${name} using Streamable HTTP`); - } catch (error) { - logger?.debug( - `Streamable HTTP failed for ${name}, falling back to SSE: ${error instanceof Error ? error.message : String(error)}`, + } + const url = new URL(server.config.url); + const headers = server.config.headers; + logger?.debug( + `Connecting to MCP server ${name} using SSE at ${url.href}`, + ); + transport = new SSEClientTransport(url, { + requestInit: { headers }, + }); + client = createClient(); + await client.connect(transport); + const toolsResponse = await client.listTools(); + tools = + toolsResponse.tools?.map((tool) => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + })) || []; + logger?.info(`Connected to MCP server ${name} using SSE`); + } else if ( + serverType === "stdio" || + (!serverType && server.config.command) + ) { + if (!server.config.command) { + throw new Error( + `MCP server ${name} with type "stdio" requires a 'command'`, ); - transport = new SSEClientTransport(url, { - requestInit: { headers }, - }); - client = createClient(); - await client.connect(transport); - - const toolsResponse = await client.listTools(); - tools = - toolsResponse.tools?.map((tool) => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - })) || []; - - logger?.info(`Connected to MCP server ${name} using SSE (fallback)`); } - } else if (server.config.command) { const agentEnv = this.container.get>("MergedEnv") || (process.env as Record); @@ -490,6 +500,11 @@ export class McpManager { description: tool.description, inputSchema: tool.inputSchema, })) || []; + } else if (serverType) { + // Unknown type value + throw new Error( + `MCP server ${name} has unknown type "${serverType}". Must be "stdio", "sse", or "http"`, + ); } else { throw new Error( `MCP server ${name} configuration must include either 'command' or 'url'`, diff --git a/packages/agent-sdk/src/types/mcp.ts b/packages/agent-sdk/src/types/mcp.ts index b48d59f1..3fd16b09 100644 --- a/packages/agent-sdk/src/types/mcp.ts +++ b/packages/agent-sdk/src/types/mcp.ts @@ -4,6 +4,7 @@ */ export interface McpServerConfig { + type?: "stdio" | "sse" | "http"; command?: string; args?: string[]; env?: Record; diff --git a/packages/agent-sdk/tests/managers/mcpManager.test.ts b/packages/agent-sdk/tests/managers/mcpManager.test.ts index 8aa05c4d..34dcfcf7 100644 --- a/packages/agent-sdk/tests/managers/mcpManager.test.ts +++ b/packages/agent-sdk/tests/managers/mcpManager.test.ts @@ -562,6 +562,7 @@ describe("McpManager", () => { const sseConfig = { mcpServers: { "sse-server": { + type: "sse", url: "https://example.com/sse", }, }, @@ -570,11 +571,6 @@ describe("McpManager", () => { vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(sseConfig)); await mcpManager.loadConfig(); - // Mock Streamable HTTP to fail to trigger fallback - vi.mocked(StreamableHTTPClientTransport).mockImplementation(function () { - throw new Error("Streamable HTTP not supported"); - }); - const result = await mcpManager.connectServer("sse-server"); expect(result).toBe(true); @@ -594,6 +590,7 @@ describe("McpManager", () => { const streamableConfig = { mcpServers: { "streamable-server": { + type: "http", url: "https://example.com/streamable", headers: { Authorization: "Bearer test-token" }, }, @@ -626,11 +623,12 @@ describe("McpManager", () => { expect(server?.status).toBe("connected"); }); - it("should fallback to SSE if Streamable HTTP fails", async () => { + it("should not fallback to SSE when type is 'http'", async () => { const config = { mcpServers: { - "fallback-server": { - url: "https://example.com/fallback", + "http-server": { + type: "http", + url: "https://example.com/http", headers: { "X-Custom": "value" }, }, }, @@ -639,24 +637,20 @@ describe("McpManager", () => { vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(config)); await mcpManager.loadConfig(); - // First call to connect (Streamable HTTP) fails + // Make client.connect fail for Streamable HTTP mockClient.connect.mockRejectedValueOnce( new Error("Streamable HTTP failed"), ); - const result = await mcpManager.connectServer("fallback-server"); + const result = await mcpManager.connectServer("http-server"); - expect(result).toBe(true); + // Should fail entirely, NOT fall back to SSE + expect(result).toBe(false); expect(StreamableHTTPClientTransport).toHaveBeenCalled(); - expect(SSEClientTransport).toHaveBeenCalledWith( - new URL("https://example.com/fallback"), - { - requestInit: { headers: { "X-Custom": "value" } }, - }, - ); + expect(SSEClientTransport).not.toHaveBeenCalled(); - const server = mcpManager.getServer("fallback-server"); - expect(server?.status).toBe("connected"); + const server = mcpManager.getServer("http-server"); + expect(server?.status).toBe("error"); }); it("should throw error if neither command nor url is provided", async () => { @@ -677,6 +671,120 @@ describe("McpManager", () => { expect(server?.error).toContain("must include either 'command' or 'url'"); }); + it("should reject unknown type value", async () => { + const invalidConfig = { + mcpServers: { + "bad-type-server": { + type: "websocket", + url: "https://example.com/ws", + }, + }, + }; + const { promises: fs } = await import("fs"); + vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(invalidConfig)); + await mcpManager.loadConfig(); + + const result = await mcpManager.connectServer("bad-type-server"); + + expect(result).toBe(false); + const server = mcpManager.getServer("bad-type-server"); + expect(server?.status).toBe("error"); + expect(server?.error).toContain('unknown type "websocket"'); + }); + + it("should default URL-based server to Streamable HTTP when type is not specified", async () => { + const config = { + mcpServers: { + "default-http-server": { + url: "https://example.com/default", + }, + }, + }; + const { promises: fs } = await import("fs"); + vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(config)); + await mcpManager.loadConfig(); + + vi.mocked(StreamableHTTPClientTransport).mockImplementation(function () { + return mockTransport as never; + }); + + const result = await mcpManager.connectServer("default-http-server"); + + expect(result).toBe(true); + expect(StreamableHTTPClientTransport).toHaveBeenCalledWith( + new URL("https://example.com/default"), + { + requestInit: { headers: undefined }, + }, + ); + expect(SSEClientTransport).not.toHaveBeenCalled(); + }); + + it("should reject http type without url", async () => { + const invalidConfig = { + mcpServers: { + "http-no-url": { + type: "http", + }, + }, + }; + const { promises: fs } = await import("fs"); + vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(invalidConfig)); + await mcpManager.loadConfig(); + + const result = await mcpManager.connectServer("http-no-url"); + + expect(result).toBe(false); + const server = mcpManager.getServer("http-no-url"); + expect(server?.status).toBe("error"); + expect(server?.error).toContain("requires a 'url'"); + }); + + it("should reject sse type without url", async () => { + const invalidConfig = { + mcpServers: { + "sse-no-url": { + type: "sse", + }, + }, + }; + const { promises: fs } = await import("fs"); + vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(invalidConfig)); + await mcpManager.loadConfig(); + + const result = await mcpManager.connectServer("sse-no-url"); + + expect(result).toBe(false); + const server = mcpManager.getServer("sse-no-url"); + expect(server?.status).toBe("error"); + expect(server?.error).toContain("requires a 'url'"); + }); + + it("should connect with explicit type 'stdio'", async () => { + const stdioConfig = { + mcpServers: { + "explicit-stdio": { + type: "stdio", + command: "npx", + args: ["explicit-stdio-server"], + }, + }, + }; + const { promises: fs } = await import("fs"); + vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(stdioConfig)); + await mcpManager.loadConfig(); + + const result = await mcpManager.connectServer("explicit-stdio"); + + expect(result).toBe(true); + expect(StdioClientTransport).toHaveBeenCalledWith( + expect.objectContaining({ + command: "npx", + args: ["explicit-stdio-server"], + }), + ); + }); + it("should handle connection failure", async () => { mockClient.connect.mockRejectedValue(new Error("Connection failed")); diff --git a/packages/code/src/acp/agent.ts b/packages/code/src/acp/agent.ts index 0e34837e..38688842 100644 --- a/packages/code/src/acp/agent.ts +++ b/packages/code/src/acp/agent.ts @@ -211,7 +211,8 @@ export class WaveAcpAgent implements AcpAgent { callbacks.onPermissionModeChange?.(mode), onModelChange: (model) => callbacks.onModelChange?.(model), onUserMessageAdded: (params) => callbacks.onUserMessageAdded?.(params), - onMcpServersChange: (servers) => callbacks.onMcpServersChange?.(servers), + onMcpServersChange: (servers) => + callbacks.onMcpServersChange?.(servers), }, }); @@ -1043,9 +1044,11 @@ function convertAcpMcpServers( for (const server of servers) { const config: McpServerConfig = {}; if ("type" in server && server.type === "http") { + config.type = "http"; config.url = server.url; config.headers = convertHttpHeaders(server.headers); } else if ("type" in server && server.type === "sse") { + config.type = "sse"; config.url = server.url; config.headers = convertHttpHeaders(server.headers); } else { diff --git a/packages/code/src/components/McpManager.tsx b/packages/code/src/components/McpManager.tsx index e5fa4b3e..bc01b1b4 100644 --- a/packages/code/src/components/McpManager.tsx +++ b/packages/code/src/components/McpManager.tsx @@ -122,6 +122,13 @@ export const McpManager: React.FC = ({ + {selectedServer.config.type && ( + + + Type: {selectedServer.config.type} + + + )} Command: {selectedServer.config.command} diff --git a/packages/code/tests/components/McpManager.test.tsx b/packages/code/tests/components/McpManager.test.tsx index 45adfc06..ffafb133 100644 --- a/packages/code/tests/components/McpManager.test.tsx +++ b/packages/code/tests/components/McpManager.test.tsx @@ -420,6 +420,30 @@ describe("McpManager", () => { expect(output).toContain("KEY2=VAL2"); }); + it("should display transport type in detail view", async () => { + const httpServer: McpServerStatus = { + name: "http-server", + config: { + type: "http", + url: "https://example.com/mcp", + }, + originalUrl: "https://example.com/mcp", + status: "connected", + toolCount: 3, + }; + + const { lastFrame, stdin } = render( + , + ); + + stdin.write("\r"); + await vi.waitFor(() => + expect(lastFrame()).toContain("MCP Server Details: http-server"), + ); + const output = lastFrame(); + expect(output).toContain("Type: http"); + }); + it("should display error message in detail view", async () => { const { lastFrame, stdin } = render(); From 2d702bca8bac3b89e19e7c60707e031d1f1f5f41 Mon Sep 17 00:00:00 2001 From: lewis617 Date: Thu, 28 May 2026 18:55:49 +0800 Subject: [PATCH 3/4] fix: update ACP agent test expectations for type field --- packages/code/tests/acp/agent.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/code/tests/acp/agent.test.ts b/packages/code/tests/acp/agent.test.ts index 2927eebb..c98b4c27 100644 --- a/packages/code/tests/acp/agent.test.ts +++ b/packages/code/tests/acp/agent.test.ts @@ -2003,6 +2003,7 @@ describe("WaveAcpAgent", () => { expect(capturedOptions!.mcpServers).toEqual({ "api-server": { + type: "http", url: "https://api.example.com/mcp", headers: { Authorization: "Bearer token123" }, }, @@ -2037,6 +2038,7 @@ describe("WaveAcpAgent", () => { expect(capturedOptions!.mcpServers).toEqual({ "event-stream": { + type: "sse", url: "https://events.example.com/mcp", headers: { "X-API-Key": "apikey456" }, }, From 93d3d290ea333092a9948ae40cf1e77627772bc0 Mon Sep 17 00:00:00 2001 From: lewis617 Date: Thu, 28 May 2026 19:02:35 +0800 Subject: [PATCH 4/4] docs: update specs and skill docs for MCP type field Update 003-mcp and 071 specs, MCP.md skill doc, and examples to reflect the new type field in McpServerConfig. Remove obsolete constructor-mcp-deferred.ts example. Add type:'http' to tavily example. --- .../agent-sdk/builtin/skills/settings/MCP.md | 53 +++++++- .../examples/constructor-mcp-deferred.ts | 124 ------------------ .../agent-sdk/examples/tavily-mcp-example.ts | 1 + specs/003-mcp/contracts/mcp-interfaces.md | 32 ++++- specs/003-mcp/data-model.md | 12 +- specs/003-mcp/quickstart.md | 13 ++ specs/003-mcp/spec.md | 12 +- specs/003-mcp/tasks.md | 4 + .../contracts/skill-interaction.md | 2 +- specs/071-builtin-settings-skill/spec.md | 2 +- 10 files changed, 111 insertions(+), 144 deletions(-) delete mode 100644 packages/agent-sdk/examples/constructor-mcp-deferred.ts diff --git a/packages/agent-sdk/builtin/skills/settings/MCP.md b/packages/agent-sdk/builtin/skills/settings/MCP.md index f48ce36c..fb937d95 100644 --- a/packages/agent-sdk/builtin/skills/settings/MCP.md +++ b/packages/agent-sdk/builtin/skills/settings/MCP.md @@ -32,25 +32,70 @@ The `.mcp.json` file contains a list of MCP server configurations. ### Fields for each server: +- `type`: (Optional) The transport type: `"stdio"`, `"sse"`, or `"http"`. If omitted, Wave infers the type from other fields (URL → `"http"`, command → `"stdio"`). Set explicitly for clarity and to avoid the default behavior. - `command`: (For stdio) The executable to run (e.g., `npx`, `uvx`, `python`, `node`). - `args`: (For stdio) An array of command-line arguments for the executable. - `env`: (Optional) A record of environment variables for the server process. -- `url`: (For SSE) The endpoint URL of a remote MCP server (e.g., `https://example.com/sse`). +- `url`: (For `sse`/`http`) The endpoint URL of a remote MCP server. +- `headers`: (For `sse`/`http`) A record of HTTP headers to send with requests (e.g., `{"Authorization": "Bearer token"}`). -## Remote MCP Servers (SSE) +## Transport Types -Wave also supports connecting to remote MCP servers via SSE (Server-Sent Events). +Wave supports three MCP transport types. When `type` is not specified, Wave uses the following defaults: +- If `url` is provided → defaults to `"http"` (Streamable HTTP) +- If `command` is provided → defaults to `"stdio"` + +### stdio + +The server is launched as a local subprocess. Use for locally installed MCP servers. + +```json +{ + "mcpServers": { + "sqlite": { + "type": "stdio", + "command": "uvx", + "args": ["mcp-server-sqlite", "--db-path", "/path/to/db"] + } + } +} +``` + +### http (Streamable HTTP) + +The recommended transport for remote servers. Uses the MCP Streamable HTTP protocol. ```json { "mcpServers": { - "remote-server": { + "remote-api": { + "type": "http", + "url": "https://mcp-server.example.com/mcp", + "headers": { + "Authorization": "Bearer your-token" + } + } + } +} +``` + +### sse (Server-Sent Events) + +Legacy transport for remote servers that only support SSE. Use `"http"` for new servers unless the server requires SSE. + +```json +{ + "mcpServers": { + "legacy-server": { + "type": "sse", "url": "https://mcp-server.example.com/sse" } } } ``` +> **Note**: When `type` is not specified, URL-based servers default to `"http"` with no SSE fallback. If you need SSE, set `type: "sse"` explicitly. + ## Using MCP Tools Once configured, Wave will automatically connect to the MCP servers when it starts. Tools provided by these servers will be available to the agent with a prefix: diff --git a/packages/agent-sdk/examples/constructor-mcp-deferred.ts b/packages/agent-sdk/examples/constructor-mcp-deferred.ts deleted file mode 100644 index 8ff1c19a..00000000 --- a/packages/agent-sdk/examples/constructor-mcp-deferred.ts +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env tsx - -/** - * Verifies that MCP servers passed via agent.create() constructor options - * are deferred and require tool_search to discover. - * - * Uses the popo-acp MCP server running at http://localhost:3100/mcp. - */ - -import fs from "fs/promises"; -import path from "path"; -import os from "os"; -import { Agent } from "../src/agent.js"; - -async function main() { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), "constructor-mcp-deferred-"), - ); - - const agent = await Agent.create({ - workdir: tempDir, - permissionMode: "bypassPermissions", - model: "gemini-2.5-flash", - mcpServers: { - "popo-acp": { - url: "http://localhost:3100/mcp", - }, - }, - callbacks: { - onToolBlockUpdated: (params) => { - if (params.stage === "start") { - console.log(`[TOOL START] ${params.name}`); - } - if (params.stage === "end") { - const status = params.success ? "✅" : "❌"; - const short = (params.result || "").slice(0, 150); - console.log(`${status} [TOOL END] ${params.name}: ${short}`); - } - }, - onAssistantContentUpdated: (chunk: string) => { - process.stdout.write(chunk); - }, - }, - }); - - try { - // Step 1: Wait for MCP connection - console.log("⏳ Waiting for MCP connection...\n"); - let attempts = 0; - while (attempts < 30) { - const servers = agent.getMcpServers(); - const popo = servers.find((s) => s.name === "popo-acp"); - if (popo?.status === "connected") { - console.log(`✅ popo-acp connected (${popo.tools?.length || 0} tools)`); - popo.tools?.forEach((t) => - console.log(` - ${t.name}: ${t.description}`), - ); - break; - } - if (popo?.status === "error") { - console.log(`❌ popo-acp error: ${popo.error}`); - break; - } - await new Promise((r) => setTimeout(r, 1000)); - attempts++; - } - - // Step 2: Ask agent to discover MCP tools via tool_search and call one - console.log("\n📨 Asking agent to discover MCP tools and send a file..."); - await agent.sendMessage( - "First use the tool_search tool to find available MCP tools. " + - "If you find a tool called mcp__popo-acp__sendFile, describe what it does. " + - "Then try to call it with a test file path like /tmp/test.txt.", - ); - - // Wait for agent to finish - let waitAttempts = 0; - while (agent.isLoading && waitAttempts < 120) { - await new Promise((r) => setTimeout(r, 1000)); - waitAttempts++; - } - - // Step 3: Check message history for MCP tool usage - console.log(`\n📊 Final state: ${agent.messages.length} messages`); - - const mcpToolsUsed = new Set(); - for (const msg of agent.messages) { - for (const block of msg.blocks) { - if ( - block.type === "tool" && - block.name?.startsWith("mcp__") && - block.name - ) { - mcpToolsUsed.add(block.name); - } - } - } - - console.log(`MCP tools called: ${mcpToolsUsed.size}`); - mcpToolsUsed.forEach((name) => console.log(` - ${name}`)); - - // Summary - console.log("\n" + "=".repeat(60)); - console.log("RESULT:"); - if (mcpToolsUsed.size > 0) { - console.log( - "✅ PASS: MCP tools from constructor were deferred and became available after tool_search", - ); - } else { - console.log( - "⚠️ UNCERTAIN: No MCP tools were called (check if tool_search succeeded)", - ); - } - } finally { - await agent.destroy(); - await fs.rm(tempDir, { recursive: true, force: true }); - process.exit(0); - } -} - -main().catch((error) => { - console.error("💥 Unhandled error:", error); - process.exit(1); -}); diff --git a/packages/agent-sdk/examples/tavily-mcp-example.ts b/packages/agent-sdk/examples/tavily-mcp-example.ts index 4e887dfe..6276b2e1 100644 --- a/packages/agent-sdk/examples/tavily-mcp-example.ts +++ b/packages/agent-sdk/examples/tavily-mcp-example.ts @@ -28,6 +28,7 @@ async function main() { const mcpConfig = { mcpServers: { tavily: { + type: "http", url: "https://mcp.tavily.com/mcp/", // Authenticate using the Authorization header with ${TAVILY_API_KEY} env var expansion headers: { diff --git a/specs/003-mcp/contracts/mcp-interfaces.md b/specs/003-mcp/contracts/mcp-interfaces.md index 9e5c4bf4..9cd1ef8c 100644 --- a/specs/003-mcp/contracts/mcp-interfaces.md +++ b/specs/003-mcp/contracts/mcp-interfaces.md @@ -4,10 +4,14 @@ ```typescript export interface McpServerConfig { + type?: "stdio" | "sse" | "http"; command?: string; args?: string[]; env?: Record; url?: string; + headers?: Record; + /** Internal: plugin directory path when the server is registered by a plugin */ + pluginRoot?: string; } export interface McpConfig { @@ -19,11 +23,13 @@ export interface McpConfig { ```typescript export interface McpServer { + name: string; command?: string; args?: string[]; - env?: Record; + env?: Array<{ name: string; value: string }>; url?: string; - transport?: "stdio" | "http" | "sse"; + headers?: Array<{ name: string; value: string }>; + type?: "stdio" | "http" | "sse"; } ``` @@ -39,7 +45,9 @@ export interface McpTool { export interface McpServerStatus { name: string; config: McpServerConfig; - status: "disconnected" | "connected" | "connecting" | "error"; + /** Pre-resolution URL with template variables preserved for safe display */ + originalUrl?: string; + status: "disconnected" | "connected" | "connecting" | "reconnecting" | "error"; tools?: McpTool[]; toolCount?: number; capabilities?: string[]; @@ -81,8 +89,24 @@ export interface McpManagerOptions { { capabilities: { mcpCapabilities: { - transports: ["http", "sse"]; + http: true, + sse: true } } } ``` + +## Transport Dispatch (connectServer) + +When connecting an MCP server, the `type` field in `McpServerConfig` determines the transport: + +| `type` value | Transport | Required fields | +|---|---|---| +| `"http"` | `StreamableHTTPClientTransport` | `url` | +| `"sse"` | `SSEClientTransport` | `url` | +| `"stdio"` | `StdioClientTransport` | `command` | +| _(omitted)_ + `url` | `StreamableHTTPClientTransport` (default) | `url` | +| _(omitted)_ + `command` | `StdioClientTransport` (default) | `command` | +| unknown | _throws error_ | — | + +**No fallback**: When `type` is `"http"` (or defaulted), a failed Streamable HTTP connection does NOT fall back to SSE. Users must set `type: "sse"` explicitly. diff --git a/specs/003-mcp/data-model.md b/specs/003-mcp/data-model.md index 4120facc..0b1cd53f 100644 --- a/specs/003-mcp/data-model.md +++ b/specs/003-mcp/data-model.md @@ -8,10 +8,13 @@ The root configuration object for MCP. ### McpServerConfig Configuration for an individual MCP server (SDK format). +- `type`: (Optional) Transport type: `"stdio"`, `"sse"`, or `"http"`. If omitted, inferred from other fields (URL → `"http"`, command → `"stdio"`). - `command`: The executable to run (for stdio transport). - `args`: (Optional) Arguments for the command. - `env`: (Optional) Environment variables for the child process. - `url`: (Optional) Endpoint URL (for http/sse transport). +- `headers`: (Optional) HTTP headers to send with requests (for http/sse transport). +- `pluginRoot`: (Optional, internal) Plugin directory path when the server is registered by a plugin. ### ACP McpServer (ACP format) Configuration from ACP clients, converted to `McpServerConfig`. @@ -52,7 +55,7 @@ Represents a tool provided by an MCP server. ### McpConnection Internal representation of an active connection. - `client`: The MCP SDK `Client` instance. -- `transport`: The `StdioClientTransport` or `SSEClientTransport` instance. +- `transport`: The `StdioClientTransport`, `SSEClientTransport`, or `StreamableHTTPClientTransport` instance. - `process`: Currently `null` as transports manage the process internally. ## Tool Execution Results @@ -71,9 +74,4 @@ Status updates sent to ACP clients via `ext_notification`. ## MCP Capabilities Advertised in ACP `initialize` response. -- `mcpCapabilities`: `{ transports: ["http", "sse"] }` -)`: Writes content to a file and returns a `` reference. - -## MCP Capabilities -Advertised in ACP `initialize` response. -- `mcpCapabilities`: `{ transports: ["http", "sse"] }` +- `mcpCapabilities`: `{ http: true, sse: true }` diff --git a/specs/003-mcp/quickstart.md b/specs/003-mcp/quickstart.md index 88b64eed..95c7acdf 100644 --- a/specs/003-mcp/quickstart.md +++ b/specs/003-mcp/quickstart.md @@ -9,11 +9,24 @@ Create a `.mcp.json` file in your project's working directory: "everything": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-everything"] + }, + "remote-api": { + "type": "http", + "url": "https://mcp-server.example.com/mcp", + "headers": { + "Authorization": "Bearer your-token" + } } } } ``` +### Transport Types + +- `"stdio"` (default when `command` is provided): Local subprocess +- `"http"` (default when `url` is provided): Streamable HTTP — recommended for remote servers +- `"sse"`: Server-Sent Events — use only for legacy servers that require SSE + ## 2. Initialize Agent The agent will automatically connect to the configured MCP servers during initialization. diff --git a/specs/003-mcp/spec.md b/specs/003-mcp/spec.md index 7671dcc2..a3ca8b6d 100644 --- a/specs/003-mcp/spec.md +++ b/specs/003-mcp/spec.md @@ -76,14 +76,16 @@ As an ACP client, I want to specify MCP servers when creating or loading a sessi - **Invalid Configuration**: If `.mcp.json` is malformed, the agent SHOULD log an error and proceed without MCP tools. - **Config Merge**: When multiple config sources exist (constructor, `.mcp.json`, plugins), they are merged with precedence: constructor > workspace (.mcp.json) > plugin servers. - **SSE Reconnection**: When an SSE EventSource connection drops unexpectedly, the system MUST attempt auto-reconnection with exponential backoff. During reconnection, the server status MUST show "reconnecting". If reconnection fails after all attempts, the status MUST transition to "error". +- **No HTTP→SSE Fallback**: When a URL-based server has `type: "http"` (or no type, which defaults to `"http"`), if the Streamable HTTP connection fails, the system MUST NOT fall back to SSE. Users who need SSE must set `type: "sse"` explicitly. +- **Unknown Type**: If `type` is set to an unrecognized value, the system MUST throw an error at connection time. ## Requirements *(mandatory)* ### Functional Requirements - **FR-001**: System MUST support loading MCP server configurations from `.mcp.json` in the working directory. -- **FR-002**: System MUST use `StdioClientTransport` or `SSEClientTransport` to communicate with MCP servers. -- **FR-010**: System MUST support remote MCP servers via SSE (Server-Sent Events) by providing a `url` in the configuration. +- **FR-002**: System MUST use `StdioClientTransport`, `SSEClientTransport`, or `StreamableHTTPClientTransport` to communicate with MCP servers based on the `type` field in `McpServerConfig`. +- **FR-010**: System MUST support remote MCP servers via SSE (Server-Sent Events) or Streamable HTTP by providing a `url` in the configuration. The transport is determined by the `type` field (`"sse"` or `"http"`). When `type` is omitted and `url` is provided, the default is `"http"` (Streamable HTTP). - **FR-003**: MCP tools MUST be registered in the `ToolManager` with the prefix `mcp__[serverName]__[toolName]`. - **FR-004**: MCP tool schemas MUST be cleaned of unsupported fields (`$schema`, `exclusiveMinimum`, `exclusiveMaximum`). - **FR-005**: `McpManager` MUST track the status of each server (connected, disconnected, error). @@ -101,6 +103,9 @@ As an ACP client, I want to specify MCP servers when creating or loading a sessi - **FR-018**: System MUST reconnect all MCP servers when SSO authentication completes (after `/login`). - **FR-019**: MCP tool execution results MUST be truncated to 50,000 characters. Excess output MUST be persisted to a file via the shared `toolResultStorage` utility, and the result MUST include a `` reference. - **FR-020**: System MUST store the pre-resolution `originalUrl` for MCP servers configured with a URL, to prevent sensitive tokens from appearing in UI status displays. +- **FR-021**: `McpServerConfig` MUST include an optional `type` field (`"stdio"`, `"sse"`, `"http"`) to explicitly specify the transport. When `type` is omitted, it is inferred: URL → `"http"`, command → `"stdio"`. System MUST NOT fall back from HTTP to SSE; the `type` must be explicit for SSE servers. +- **FR-022**: ACP bridge MUST preserve the `type` field when converting ACP `McpServer[]` to SDK `McpServerConfig` format. +- **FR-023**: MCP manager UI MUST display the transport `type` in the server detail view when present. ### Key Entities *(include if feature involves data)* @@ -109,7 +114,8 @@ As an ACP client, I want to specify MCP servers when creating or loading a sessi - `command`: Executable to run (for stdio). - `args`: Command-line arguments (for stdio). - `env`: Environment variables (for stdio). - - `url`: Endpoint URL (for SSE). + - `url`: Endpoint URL (for http/sse). + - `type`: Transport type (`"stdio"`, `"http"`, `"sse"`). If omitted, inferred from other fields. - `originalUrl`: The original URL before any resolution/substitution (for display purposes). - `status`: Current connection state. - `transport`: Transport type (`stdio`, `http`, `sse`). (ACP format) diff --git a/specs/003-mcp/tasks.md b/specs/003-mcp/tasks.md index 3a3f972a..aea78625 100644 --- a/specs/003-mcp/tasks.md +++ b/specs/003-mcp/tasks.md @@ -27,3 +27,7 @@ - [ ] Add MCP server reconnection after SSO login - [ ] Add MCP tool result size limiting (50K chars) with shared persistence utility - [ ] Add originalUrl storage for URL-based MCP servers to prevent sensitive URL exposure +- [x] Add `type` field to `McpServerConfig` for explicit transport selection (`"stdio" | "sse" | "http"`) +- [x] Replace HTTP→SSE fallback with explicit type-based transport dispatch +- [x] Preserve `type` field in ACP `convertAcpMcpServers` conversion +- [x] Display transport `type` in MCP manager UI detail view diff --git a/specs/071-builtin-settings-skill/contracts/skill-interaction.md b/specs/071-builtin-settings-skill/contracts/skill-interaction.md index abb07acb..5891a441 100644 --- a/specs/071-builtin-settings-skill/contracts/skill-interaction.md +++ b/specs/071-builtin-settings-skill/contracts/skill-interaction.md @@ -29,7 +29,7 @@ The `settings` skill is a builtin skill that helps users manage their Wave confi - `models`: Model-specific configuration overrides. ### Other Files -- `.mcp.json`: Configure external MCP servers. +- `.mcp.json`: Configure external MCP servers (supports `type`: `"stdio"`, `"sse"`, `"http"`). - `.wave/rules/*.md`: Define context-specific instructions (Memory Rules). - `.wave/skills/`: Create and manage custom skills. - `.wave/agents/`: Create and manage specialized subagents. diff --git a/specs/071-builtin-settings-skill/spec.md b/specs/071-builtin-settings-skill/spec.md index b4896a03..67c401a1 100644 --- a/specs/071-builtin-settings-skill/spec.md +++ b/specs/071-builtin-settings-skill/spec.md @@ -62,7 +62,7 @@ As a user, I want detailed documentation for complex hook configurations to be a - **FR-001**: System MUST provide a builtin `settings` skill. - **FR-002**: The `settings` skill MUST be able to read and display merged configurations from user, project, and local scopes. - **FR-003**: The `settings` skill MUST allow users to update settings in specific scopes (user, project, or local). -- **FR-004**: The `settings` skill MUST provide a guide on how to configure Wave, covering all supported fields in `settings.json` (`hooks`, `env`, `permissions`, `enabledPlugins`, `language`, `autoMemoryEnabled`, `autoMemoryFrequency`, `models`) and other configuration files (`.mcp.json` for MCP servers, `.wave/rules/` for memory rules, `.wave/skills/` for custom skills, and `.wave/agents/` for subagents). +- **FR-004**: The `settings` skill MUST provide a guide on how to configure Wave, covering all supported fields in `settings.json` (`hooks`, `env`, `permissions`, `enabledPlugins`, `language`, `autoMemoryEnabled`, `autoMemoryFrequency`, `models`) and other configuration files (`.mcp.json` for MCP servers including the `type` transport field, `.wave/rules/` for memory rules, `.wave/skills/` for custom skills, and `.wave/agents/` for subagents). - **FR-005**: System MUST include a `SKILL.md` for the `settings` skill. - **FR-006**: System MUST create separate markdown files (e.g., `HOOKS.md`, `ENV.md`, `MCP.md`, `MEMORY_RULES.md`, `SKILLS.md`, `SUBAGENTS.md`, `MODELS.md`) for complex configurations and link them from `SKILL.md`. - **FR-008**: The `settings` skill MUST provide guidance on how to create and manage custom skills and subagents.