From e7bd06a2f9b7403b767de11bf5cd57e7d4bc84ef Mon Sep 17 00:00:00 2001 From: Kent Wang Date: Thu, 11 Sep 2025 16:19:16 +0800 Subject: [PATCH] feat: Add basic response pane for MCP (#9131) * New List page * Add a common utility function * Add response pane ui * Support events and logs --- package-lock.json | 86 +++- packages/insomnia/src/common/mcp-utils.ts | 108 +++++ packages/insomnia/src/entry.preload.ts | 3 +- packages/insomnia/src/main/ipc/electron.ts | 4 +- packages/insomnia/src/main/network/mcp.ts | 376 ++++++++++++++---- packages/insomnia/src/models/mcp-response.ts | 4 + .../insomnia/src/models/request-version.ts | 8 +- ...request.$requestId.response.delete-all.tsx | 3 + ...bug.request.$requestId.response.delete.tsx | 63 +-- ...spaceId.mcp.request.$requestId.connect.tsx | 1 + ....$projectId.workspace.$workspaceId.mcp.tsx | 230 ++++++----- .../dropdowns/mcp-actions-dropdown.tsx | 121 ++++++ .../dropdowns/response-history-dropdown.tsx | 35 +- packages/insomnia/src/ui/components/icon.tsx | 3 +- .../src/ui/components/mcp/event-view.tsx | 121 ++++++ .../ui/components/mcp/mcp-request-pane.tsx | 4 +- .../insomnia/src/ui/components/mcp/types.ts | 18 + .../src/ui/components/panes/response-pane.tsx | 9 +- .../components/websockets/event-log-view.tsx | 19 +- .../websockets/realtime-response-pane.tsx | 67 ++-- .../hooks/use-realtime-connection-events.ts | 3 +- 21 files changed, 1043 insertions(+), 243 deletions(-) create mode 100644 packages/insomnia/src/common/mcp-utils.ts create mode 100644 packages/insomnia/src/ui/components/dropdowns/mcp-actions-dropdown.tsx create mode 100644 packages/insomnia/src/ui/components/mcp/event-view.tsx create mode 100644 packages/insomnia/src/ui/components/mcp/types.ts diff --git a/package-lock.json b/package-lock.json index c6326108cf..d1ccb01b07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4153,6 +4153,7 @@ "version": "1.17.5", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -4176,6 +4177,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, "license": "MIT", "dependencies": { "mime-types": "^3.0.0", @@ -4189,6 +4191,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -4205,6 +4208,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -4225,6 +4229,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -4237,6 +4242,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.6.0" @@ -4246,6 +4252,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, "license": "MIT", "dependencies": { "accepts": "^2.0.0", @@ -4288,6 +4295,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -4305,6 +4313,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -4314,12 +4323,14 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -4329,6 +4340,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4341,6 +4353,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -4353,6 +4366,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4362,6 +4376,7 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -4377,6 +4392,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -4392,6 +4408,7 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -4408,6 +4425,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.5", @@ -4430,6 +4448,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -4445,6 +4464,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -11211,6 +11231,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -11711,6 +11732,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, "license": "MIT" }, "node_modules/array-includes": { @@ -12145,6 +12167,7 @@ "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -12169,6 +12192,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -12178,6 +12202,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -12187,6 +12212,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -12199,6 +12225,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, "license": "MIT" }, "node_modules/boolbase": { @@ -12468,6 +12495,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -13457,6 +13485,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13473,6 +13502,7 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13496,6 +13526,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, "license": "MIT" }, "node_modules/cookies": { @@ -13569,6 +13600,7 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4", @@ -14238,6 +14270,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8", @@ -14586,6 +14619,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, "license": "MIT" }, "node_modules/ejs": { @@ -14974,6 +15008,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -15580,6 +15615,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -16067,6 +16103,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -16111,6 +16148,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -16123,6 +16161,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -16169,6 +16208,7 @@ "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -16225,6 +16265,7 @@ "version": "7.5.1", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 16" @@ -16240,6 +16281,7 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -16249,6 +16291,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -16258,6 +16301,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -16267,6 +16311,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, "license": "MIT" }, "node_modules/ext": { @@ -16557,6 +16602,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -16575,6 +16621,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -16584,6 +16631,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, "license": "MIT" }, "node_modules/find-up": { @@ -16750,6 +16798,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -16780,6 +16829,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -17789,6 +17839,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, "license": "MIT", "dependencies": { "depd": "2.0.0", @@ -17805,6 +17856,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -18263,6 +18315,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -20384,6 +20437,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -20413,6 +20467,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -20432,6 +20487,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -21032,6 +21088,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -22195,6 +22252,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -22491,6 +22549,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -22648,6 +22707,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, "license": "MIT" }, "node_modules/pathe": { @@ -22795,6 +22855,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.20.0" @@ -23383,6 +23444,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -23449,6 +23511,7 @@ "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" @@ -23522,6 +23585,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -23531,6 +23595,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -23546,6 +23611,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -24187,6 +24253,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -24203,6 +24270,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -24212,12 +24280,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, "license": "MIT" }, "node_modules/router/node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -24442,6 +24512,7 @@ "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -24466,6 +24537,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -24475,12 +24547,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, "license": "MIT" }, "node_modules/send/node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -24490,6 +24564,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -24499,6 +24574,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -24551,6 +24627,7 @@ "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", @@ -25317,6 +25394,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -26576,6 +26654,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -26824,6 +26903,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -26972,6 +27052,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -27076,6 +27157,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -28470,6 +28552,7 @@ "version": "3.25.75", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.75.tgz", "integrity": "sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -28479,6 +28562,7 @@ "version": "3.24.6", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.24.1" @@ -28496,7 +28580,6 @@ "@getinsomnia/node-libcurl": "3.0.0", "@grpc/grpc-js": "^1.13.3", "@grpc/proto-loader": "^0.7.13", - "@modelcontextprotocol/sdk": "^1.17.5", "@seald-io/nedb": "^4.1.1", "@segment/analytics-node": "2.2.1", "@stoplight/spectral-core": "^1.20.0", @@ -28559,6 +28642,7 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@getinsomnia/api-client": "0.0.10", "@getinsomnia/srp-js": "1.0.0-alpha.1", + "@modelcontextprotocol/sdk": "^1.17.5", "@react-router/dev": "^7.7.0", "@react-router/fs-routes": "^7.7.0", "@react-router/node": "^7.7.0", diff --git a/packages/insomnia/src/common/mcp-utils.ts b/packages/insomnia/src/common/mcp-utils.ts new file mode 100644 index 0000000000..d05dba6a5b --- /dev/null +++ b/packages/insomnia/src/common/mcp-utils.ts @@ -0,0 +1,108 @@ +import { + CallToolRequestSchema, + CallToolResultSchema, + GetPromptRequestSchema, + GetPromptResultSchema, + InitializeRequestSchema, + InitializeResultSchema, + type JSONRPCMessage, + ListPromptsRequestSchema, + ListPromptsResultSchema, + ListResourcesRequestSchema, + ListResourcesResultSchema, + ListResourceTemplatesRequestSchema, + ListResourceTemplatesResultSchema, + ListToolsRequestSchema, + ListToolsResultSchema, + type Prompt, + ReadResourceRequestSchema, + ReadResourceResultSchema, + type Resource, + type ResourceTemplate, + type ServerCapabilities, + type Tool, +} from '@modelcontextprotocol/sdk/types.js'; + +// method constants +export const METHOD_INITIALIZE = InitializeRequestSchema.shape.method.value; +export const METHOD_LIST_TOOLS = ListToolsRequestSchema.shape.method.value; +export const METHOD_LIST_RESOURCES = ListResourcesRequestSchema.shape.method.value; +export const METHOD_LIST_RESOURCE_TEMPLATES = ListResourceTemplatesRequestSchema.shape.method.value; +export const METHOD_LIST_PROMPTS = ListPromptsRequestSchema.shape.method.value; +export const METHOD_CALL_TOOL = CallToolRequestSchema.shape.method.value; +export const METHOD_READ_RESOURCE = ReadResourceRequestSchema.shape.method.value; +export const METHOD_GET_PROMPT = GetPromptRequestSchema.shape.method.value; +const METHOD_UNKNOWN = 'unknown'; +export const MCP_JSONRPC_METHODS = [ + METHOD_INITIALIZE, + METHOD_LIST_TOOLS, + METHOD_LIST_RESOURCES, + METHOD_LIST_RESOURCE_TEMPLATES, + METHOD_LIST_PROMPTS, + METHOD_CALL_TOOL, + METHOD_READ_RESOURCE, + METHOD_GET_PROMPT, +]; + +export type JSONRPCMessageMethods = + | typeof METHOD_INITIALIZE + | typeof METHOD_LIST_TOOLS + | typeof METHOD_LIST_RESOURCES + | typeof METHOD_LIST_RESOURCE_TEMPLATES + | typeof METHOD_LIST_PROMPTS + | typeof METHOD_CALL_TOOL + | typeof METHOD_READ_RESOURCE + | typeof METHOD_GET_PROMPT; + +export interface McpServerData { + serverCapabilities: ServerCapabilities; + primitives: { + tools: Tool[]; + resources: Resource[]; + resourceTemplates: ResourceTemplate[]; + prompts: Prompt[]; + }; +} + +export const getMcpMethodFromMessage = (message: JSONRPCMessage): JSONRPCMessageMethods | typeof METHOD_UNKNOWN => { + let method: JSONRPCMessageMethods | typeof METHOD_UNKNOWN = METHOD_UNKNOWN; + if ('result' in message) { + const messageResult = message.result; + if (InitializeResultSchema.safeParse(messageResult).success) { + method = METHOD_INITIALIZE; + } else if (ListToolsResultSchema.safeParse(messageResult).success) { + method = METHOD_LIST_TOOLS; + } else if (ListResourcesResultSchema.safeParse(messageResult).success) { + method = METHOD_LIST_RESOURCES; + } else if (ListResourceTemplatesResultSchema.safeParse(messageResult).success) { + method = METHOD_LIST_RESOURCE_TEMPLATES; + } else if (ListPromptsResultSchema.safeParse(messageResult).success) { + method = METHOD_LIST_PROMPTS; + } else if (CallToolResultSchema.safeParse(messageResult).success) { + method = METHOD_CALL_TOOL; + } else if (ReadResourceResultSchema.safeParse(messageResult).success) { + method = METHOD_READ_RESOURCE; + } else if (GetPromptResultSchema.safeParse(messageResult).success) { + method = METHOD_GET_PROMPT; + } + } + return method; +}; + +export const getDefaultServerCapabilities = () => { + return { + tools: { + enabled: false, + listChanged: false, + }, + resources: { + enabled: false, + listChanged: false, + subscribe: true, + }, + prompts: { + enabled: false, + listChanged: false, + }, + }; +}; diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index 4b6229ab62..04a144ff70 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -52,7 +52,7 @@ const socketIO: SocketIOBridgeAPI = { const mcp: McpBridgeAPI = { connect: options => ipcRenderer.invoke('mcp.connect', options), - close: options => ipcRenderer.send('mcp.close', options), + close: options => ipcRenderer.invoke('mcp.close', options), closeAll: () => ipcRenderer.send('mcp.closeAll'), primitive: { listTools: options => ipcRenderer.invoke('mcp.primitive.listTools', options), @@ -70,7 +70,6 @@ const mcp: McpBridgeAPI = { event: { findMany: options => ipcRenderer.invoke('mcp.event.findMany', options), }, - getServerData: options => ipcRenderer.invoke('mcp.getServerData', options), }; const grpc: gRPCBridgeAPI = { diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index 12064ac13d..afee48adbb 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -105,7 +105,8 @@ export type HandleChannels = | 'mcp.primitive.readResource' | 'mcp.primitive.subscribeResource' | 'mcp.readyState' - | 'mcp.getServerData'; + | 'mcp.event.findMany' + | 'mcp.close'; export const ipcMainHandle = ( channel: HandleChannels, @@ -148,7 +149,6 @@ export type MainOnChannels = | 'updateLatestStepName' | 'webSocket.close' | 'webSocket.closeAll' - | 'mcp.close' | 'mcp.closeAll' | 'mcp.sendMCPRequest' | 'writeText'; diff --git a/packages/insomnia/src/main/network/mcp.ts b/packages/insomnia/src/main/network/mcp.ts index 85928c9418..597fd6d747 100644 --- a/packages/insomnia/src/main/network/mcp.ts +++ b/packages/insomnia/src/main/network/mcp.ts @@ -1,30 +1,42 @@ +import fs from 'node:fs'; +import path from 'node:path'; + import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; -import type { - ClientRequest, - Prompt, - Resource, - ResourceTemplate, - ServerCapabilities, - Tool, +import { + type ClientRequest, + isInitializeRequest, + type JSONRPCMessage, + type JSONRPCResponse, } from '@modelcontextprotocol/sdk/types.js'; import electron, { BrowserWindow } from 'electron'; +import { v4 as uuidV4 } from 'uuid'; import type { z } from 'zod'; import { getAppVersion, getProductName } from '~/common/constants'; +import { getMcpMethodFromMessage } from '~/common/mcp-utils'; +import { generateId } from '~/common/misc'; +import * as models from '~/models'; import type { TransportType } from '~/models/mcp-request'; +import type { McpResponse } from '~/models/mcp-response'; import type { RequestAuthentication, RequestHeader } from '~/models/request'; import { getBasicAuthHeader } from '~/network/basic-auth/get-header'; import { getBearerAuthHeader } from '~/network/bearer-auth/get-header'; +import { invariant } from '~/utils/invariant'; import { ipcMainHandle, ipcMainOn } from '../ipc/electron'; +// Refer the SDK: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/shared/protocol.ts#L504 +// The Client type has missing transport property +type McpClient = Client & { transport: StreamableHTTPClientTransport }; +// Mcp connection and request options interface CommonMcpOptions { requestId: string; } export interface OpenMcpClientConnectionOptions extends CommonMcpOptions { url: string; requestId: string; + workspaceId: string; transportType: TransportType; headers: RequestHeader[]; authentication: RequestAuthentication; @@ -39,21 +51,56 @@ interface CallToolOptions extends CommonMcpOptions { name: string; parameters: Record; } -export interface McpServerData { - serverCapabilities: ServerCapabilities; - primitives: { - tools: Tool[]; - resources: Resource[]; - resourceTemplates: ResourceTemplate[]; - prompts: Prompt[]; - }; + +interface McpCloseEvent { + _id: string; + requestId: string; + type: 'close'; + timestamp: number; + reason: string; +} +export interface McpMessageEvent { + _id: string; + requestId: string; + type: 'message'; + direction: 'INCOMING'; + timestamp: number; + data: JSONRPCResponse; + method: string; +} +interface McpErrorEvent { + _id: string; + requestId: string; + timestamp: number; + type: 'error'; + message: string; + error: any; +} +interface McpRequestEvent { + _id: string; + requestId: string; + type: 'message'; + timestamp: number; + direction: 'OUTGOING'; + method: string; + data: any; +} +export type McpEvent = McpMessageEvent | McpRequestEvent | McpCloseEvent | McpErrorEvent; +interface ResponseEventOptions { + responseId: string; + requestId: string; + environmentId: string | null; + timelinePath: string; + eventLogPath: string; } -const mcpConnections = new Map(); -// In-memory store of mcp server capabilities, tools/resources/resource templates/prompts list data for each mcp request -const mcpServerDataStore = new Map(); +const mcpConnections = new Map(); +const eventLogFileStreams = new Map(); +const timelineFileStreams = new Map(); -const mcpStateChannelBuilder = (requestId: string) => `mcp.${requestId}.readyState`; +const protocol = 'mcp'; +const getMcpStateChannel = (requestId: string) => `${protocol}.${requestId}.readyState`; +const mcpEventIdGenerator = () => `mcp-${uuidV4()}`; const _getMcpClient = (id: string) => { const mcpClient = mcpConnections.get(id); if (!mcpClient) { @@ -68,13 +115,185 @@ const _notifyMcpClientStateChange = (channel: string, isConnected: boolean) => { } }; -const _clearMcpMaps = (requestId: string) => { +const _clearMcpMaps = (requestId: string, timelineMessage: string, event?: McpEvent) => { + if (event) { + eventLogFileStreams.get(requestId)?.write(JSON.stringify(event) + '\n'); + } + eventLogFileStreams.get(requestId)?.end(); + eventLogFileStreams.delete(requestId); + timelineFileStreams + .get(requestId) + ?.write(JSON.stringify({ value: timelineMessage, name: 'Text', timestamp: Date.now() }) + '\n'); + timelineFileStreams.get(requestId)?.end(); + timelineFileStreams.delete(requestId); mcpConnections.delete(requestId); - mcpServerDataStore.delete(requestId); +}; + +const _handleCloseMcpConnection = (requestId: string, error?: Error) => { + if (error) { + const closeEvent: McpErrorEvent = { + _id: mcpEventIdGenerator(), + requestId, + type: 'error', + timestamp: Date.now(), + error, + message: error.message || 'Unknown error', + }; + // clear in-memory store + _clearMcpMaps(requestId, 'Closed MCP connection', closeEvent); + } else { + const closeEvent: McpCloseEvent = { + _id: mcpEventIdGenerator(), + requestId, + type: 'close', + timestamp: Date.now(), + reason: 'Mcp connection closed', + }; + // clear in-memory store + _clearMcpMaps(requestId, 'Closed MCP connection', closeEvent); + } + + const mcpStateChannel = getMcpStateChannel(requestId); + // notify renderer process about state change + _notifyMcpClientStateChange(mcpStateChannel, false); +}; + +const _handleMcpConnectionError = (requestId: string, error: Error) => { + const messageEvent: McpErrorEvent = { + _id: mcpEventIdGenerator(), + requestId, + type: 'error', + message: error.message || 'Unknown error', + error, + timestamp: Date.now(), + }; + eventLogFileStreams.get(requestId)?.write(JSON.stringify(messageEvent) + '\n'); + console.error(`MCP connection error for requestId: ${requestId}`, error); + // _handleCloseMcpConnection(requestId); +}; + +const _handleMcpMessage = (message: JSONRPCMessage, requestId: string) => { + const method = getMcpMethodFromMessage(message); + const messageEvent: McpMessageEvent = { + _id: mcpEventIdGenerator(), + requestId, + type: 'message', + method, + data: message as JSONRPCResponse, + direction: 'INCOMING', + timestamp: Date.now(), + }; + eventLogFileStreams.get(requestId)?.write(JSON.stringify(messageEvent) + '\n'); +}; + +const createErrorResponse = async ({ + requestId, + responseId, + environmentId, + timelinePath, + message, +}: ResponseEventOptions & { message: string }) => { + const settings = await models.settings.get(); + const responsePatch = { + _id: responseId, + parentId: requestId, + environmentId: environmentId, + timelinePath, + statusMessage: 'Error', + error: message, + }; + const res = await models.mcpResponse.create(responsePatch, settings.maxHistoryResponses); + models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: res._id }); +}; + +const getInitialTimeline = (url: string) => { + return [ + { value: `Preparing request to ${url}`, name: 'Text', timestamp: Date.now() }, + { value: `Current time is ${new Date().toISOString()}`, name: 'Text', timestamp: Date.now() }, + ]; +}; +const parseResponseAndBuildTimeline = (requestHeaderLogs: string, response: Response) => { + const statusMessage = response.statusText || ''; + const statusCode = response.status || 0; + const responseHeaders: { name: string; value: string }[] = [...response.headers.entries()].map(([name, value]) => ({ + name, + value, + })); + + const headersIn = responseHeaders.map(({ name, value }) => `${name}: ${value}`).join('\n'); + const timeline = [ + { value: requestHeaderLogs, name: 'HeaderOut', timestamp: Date.now() }, + { value: `${statusCode} ${statusMessage}`, name: 'HeaderIn', timestamp: Date.now() }, + { value: headersIn, name: 'HeaderIn', timestamp: Date.now() }, + ]; + return { timeline, responseHeaders, statusCode, statusMessage }; +}; + +// A wrapper fetch to log request and response details +const fetchWithLogging = async ( + url: string | URL, + init: RequestInit, + { requestId, responseId, environmentId, timelinePath, eventLogPath }: ResponseEventOptions, +) => { + const { method = 'GET' } = init; + const reqHeader = new Headers(init?.headers || {}); + const isJsonRequest = reqHeader.get('content-type')?.toLowerCase().includes('application/json'); + const requestBody = isJsonRequest ? JSON.parse(init.body?.toString() || '{}') : init.body?.toString() || ''; + const isMcpInitializeRequest = isJsonRequest && isInitializeRequest(requestBody); + if (isMcpInitializeRequest) { + // Add initial timeline + const initialTimelines = getInitialTimeline(url.toString()); + initialTimelines.map(t => timelineFileStreams.get(requestId)?.write(JSON.stringify(t) + '\n')); + } + const requestHeaders: { name: string; value: string }[] = [...reqHeader.entries()].map(([name, value]) => ({ + name, + value, + })); + const requestMethodLine = `${method.toUpperCase()} ${url} ${isJsonRequest && requestBody?.method ? `\nJSON-RPC Method: ${requestBody.method}` : ''}`; + const headersOut = requestHeaders.map(({ name, value }) => `${name}: ${value}`).join('\n'); + const start = performance.now(); + const response = await fetch(url, init); + const { timeline, responseHeaders, statusCode, statusMessage } = parseResponseAndBuildTimeline( + `${requestMethodLine}\n${headersOut}`, + response, + ); + timeline.map(t => timelineFileStreams.get(requestId)?.write(JSON.stringify(t) + '\n')); + if (isMcpInitializeRequest) { + // Create response model only for initialize response + const responsePatch: Partial = { + _id: responseId, + parentId: requestId, + environmentId, + headers: responseHeaders, + url: url.toString(), + statusCode, + statusMessage, + elapsedTime: performance.now() - start, + timelinePath, + eventLogPath, + }; + const settings = await models.settings.get(); + const res = await models.mcpResponse.create(responsePatch, settings.maxHistoryResponses); + models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: res._id }); + } + if (requestBody) { + // Add request event + const requestEvent: McpRequestEvent = { + _id: mcpEventIdGenerator(), + method: requestBody.method, + requestId, + type: 'message', + direction: 'OUTGOING', + timestamp: Date.now(), + data: requestBody, + }; + eventLogFileStreams.get(requestId)?.write(JSON.stringify(requestEvent) + '\n'); + } + return response; }; const openMcpClientConnection = async (options: OpenMcpClientConnectionOptions) => { - const { transportType, url, requestId } = options; + const { transportType, url, requestId, workspaceId } = options; if (!url) { throw new Error('MCP server url is required'); } @@ -103,20 +322,47 @@ const openMcpClientConnection = async (options: OpenMcpClientConnectionOptions) .filter(({ name, disabled }) => Boolean(name) && !disabled) .reduce(reduceArrayToLowerCaseKeyedDictionary, {}); + // create response model and file streams + const responseId = generateId('res'); + const responsesDir = path.join(process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'), 'responses'); + const eventLogPath = path.join(responsesDir, uuidV4() + '.response'); + eventLogFileStreams.set(requestId, fs.createWriteStream(eventLogPath)); + const timelinePath = path.join(responsesDir, responseId + '.timeline'); + timelineFileStreams.set(requestId, fs.createWriteStream(timelinePath)); + const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId); + // fallback to base environment + const activeEnvironmentId = workspaceMeta.activeEnvironmentId; + const activeEnvironment = activeEnvironmentId && (await models.environment.getById(activeEnvironmentId)); + const environment = activeEnvironment || (await models.environment.getOrCreateForParentId(workspaceId)); + invariant(environment, 'failed to find environment ' + activeEnvironmentId); + const responseEnvironmentId = environment ? environment._id : null; + + // create connection const mcpClient = new Client({ name: getProductName(), version: getAppVersion(), }); - const mcpStateChannel = mcpStateChannelBuilder(requestId); + mcpClient.onclose = () => _handleCloseMcpConnection(requestId); + mcpClient.onerror = _error => _handleMcpConnectionError(requestId, _error); + const mcpStateChannel = getMcpStateChannel(requestId); + let transport: StreamableHTTPClientTransport; switch (transportType) { case 'streamable-http': { try { const mcpServerUrl = new URL(url); - const transport = new StreamableHTTPClientTransport(mcpServerUrl, { + transport = new StreamableHTTPClientTransport(mcpServerUrl, { requestInit: { headers: lowerCasedEnabledHeaders, }, + fetch: (url, init) => + fetchWithLogging(url, init || {}, { + requestId, + responseId, + environmentId: responseEnvironmentId, + timelinePath, + eventLogPath, + }), reconnectionOptions: { maxReconnectionDelay: 30000, initialReconnectionDelay: 1000, @@ -124,73 +370,56 @@ const openMcpClientConnection = async (options: OpenMcpClientConnectionOptions) maxRetries: 2, }, }); + transport.onmessage = message => _handleMcpMessage(message, requestId); await mcpClient.connect(transport); - mcpClient.onclose = () => { - // terminate the session when client is closed - transport.terminateSession(); - // clear in-memory store - _clearMcpMaps(requestId); - // notify renderer process about state change - _notifyMcpClientStateChange(mcpStateChannel, false); - }; - mcpClient.onerror = _error => { - // TODO support error - }; - break; } catch (error) { - throw new Error(`Failed to create Streamable HTTP transport: ${error}`); + // Log error when connection fails with exception + createErrorResponse({ + requestId, + responseId, + environmentId: responseEnvironmentId, + timelinePath, + eventLogPath, + message: error.message || 'Something went wrong', + }); + console.error(`Failed to create Streamable HTTP transport: ${error}`); + return; } + break; } - default: { throw new Error(`Unsupported transport type: ${transportType}`); } } - mcpConnections.set(requestId, mcpClient); + mcpConnections.set(requestId, mcpClient as McpClient); const serverCapabilities = mcpClient.getServerCapabilities(); - let tools: Tool[] = []; - let resources: Resource[] = []; - let resourceTemplates: ResourceTemplate[] = []; - let prompts: Prompt[] = []; const primitivePromises: Promise[] = []; // get server primitives if supported if (serverCapabilities?.tools) { - primitivePromises.push(mcpClient.listTools().then(response => (tools = response.tools))); + primitivePromises.push(mcpClient.listTools()); } if (serverCapabilities?.resources) { - primitivePromises.push(mcpClient.listResources().then(response => (resources = response.resources))); - primitivePromises.push( - mcpClient.listResourceTemplates().then(response => (resourceTemplates = response.resourceTemplates)), - ); + primitivePromises.push(mcpClient.listResources()); + primitivePromises.push(mcpClient.listResourceTemplates()); } if (serverCapabilities?.prompts) { - primitivePromises.push(mcpClient.listPrompts().then(response => (prompts = response.prompts))); + primitivePromises.push(mcpClient.listPrompts()); } try { await Promise.all(primitivePromises); } catch (error) { console.warn('Failed to fetch one or more primitive types from MCP server', error); } - const serverData = { - serverCapabilities: serverCapabilities, - primitives: { - tools, - resources, - resourceTemplates, - prompts, - }, - }; - mcpServerDataStore.set(requestId, serverData as McpServerData); // notify connection ready after capabilities and primitives are fetched _notifyMcpClientStateChange(mcpStateChannel, true); - return serverData; }; -const closeMcpConnection = (options: CommonMcpOptions) => { +const closeMcpConnection = async (options: CommonMcpOptions) => { const { requestId } = options; const mcpClient = _getMcpClient(requestId); if (mcpClient) { + await mcpClient.transport.terminateSession(); mcpClient.close(); } }; @@ -201,10 +430,22 @@ const closeAllMcpConnections = () => { } }; -const getServerData = async (options: CommonMcpOptions) => mcpServerDataStore.get(options.requestId); - -const findMany = async (_options: { responseId: string }): Promise => { - return []; +const findMany = async (options: { responseId: string }): Promise => { + const response = await models.mcpResponse.getById(options.responseId); + if (!response || !response.eventLogPath) { + return []; + } + const body = await fs.promises.readFile(response.eventLogPath); + return ( + body + .toString() + .split('\n') + .filter(e => e?.trim()) + // Parse the message + .map(e => JSON.parse(e)) + // Reverse the list of messages so that we get the latest message first + .reverse() || [] + ); }; const listTools = async (options: CommonMcpOptions) => { @@ -269,7 +510,6 @@ export interface McpBridgeAPI { connect: typeof openMcpClientConnection; close: typeof closeMcpConnection; closeAll: typeof closeAllMcpConnections; - getServerData: typeof getServerData; primitive: { listTools: typeof listTools; callTool: typeof callTool; @@ -308,10 +548,10 @@ export const registerMcpHandlers = () => { ipcMainHandle('mcp.primitive.subscribeResource', (_, options: Parameters[0]) => subscribeResource(options), ); - ipcMainOn('mcp.close', (_, options: Parameters[0]) => closeMcpConnection(options)); + ipcMainHandle('mcp.close', (_, options: Parameters[0]) => closeMcpConnection(options)); ipcMainOn('mcp.closeAll', closeAllMcpConnections); ipcMainHandle('mcp.readyState', (_, options: Parameters[0]) => getMcpReadyState(options)); - ipcMainHandle('mcp.getServerData', (_, options: Parameters[0]) => getServerData(options)); + ipcMainHandle('mcp.event.findMany', (_, options: Parameters[0]) => findMany(options)); }; electron.app.on('window-all-closed', closeAllMcpConnections); diff --git a/packages/insomnia/src/models/mcp-response.ts b/packages/insomnia/src/models/mcp-response.ts index e0b03504ff..6f79b07efc 100644 --- a/packages/insomnia/src/models/mcp-response.ts +++ b/packages/insomnia/src/models/mcp-response.ts @@ -14,6 +14,8 @@ export const canSync = false; export interface BaseMcpResponse { environmentId: string | null; + statusCode: number; + statusMessage: string; url: string; elapsedTime: number; headers: ResponseHeader[]; @@ -37,6 +39,8 @@ export function init(): BaseMcpResponse { timelinePath: '', eventLogPath: '', error: '', + statusCode: 0, + statusMessage: '', requestVersionId: null, environmentId: null, }; diff --git a/packages/insomnia/src/models/request-version.ts b/packages/insomnia/src/models/request-version.ts index 5ad3cea111..037f5f43ce 100644 --- a/packages/insomnia/src/models/request-version.ts +++ b/packages/insomnia/src/models/request-version.ts @@ -5,7 +5,7 @@ import { compressObject, decompressObject } from '../common/misc'; import * as requestOperations from '../models/helpers/request-operations'; import type { GrpcRequest } from './grpc-request'; import type { BaseModel } from './index'; -import type { McpRequest } from './mcp-request'; +import { isMcpRequest, type McpRequest } from './mcp-request'; import { isRequest, type Request } from './request'; import { isSocketIORequest, type SocketIORequest } from './socket-io-request'; import { isWebSocketRequest, type WebSocketRequest } from './websocket-request'; @@ -58,7 +58,7 @@ export function findByParentId(parentId: string) { } export async function create(request: Request | WebSocketRequest | GrpcRequest | SocketIORequest | McpRequest) { - if (!isRequest(request) && !isWebSocketRequest(request) && !isSocketIORequest(request)) { + if (!isRequest(request) && !isWebSocketRequest(request) && !isSocketIORequest(request) && !isMcpRequest(request)) { throw new Error(`New ${type} was not given a valid ${request.type} instance`); } @@ -118,8 +118,8 @@ export async function restore(requestVersionId: string) { return requestOperations.update(originalRequest, requestPatch); } function _diffRequests( - rOld: Request | WebSocketRequest | SocketIORequest | null, - rNew: Request | WebSocketRequest | SocketIORequest, + rOld: Request | WebSocketRequest | SocketIORequest | McpRequest | null, + rNew: Request | WebSocketRequest | SocketIORequest | McpRequest, ) { if (!rOld) { return true; diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete-all.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete-all.tsx index b3b2e9188f..c4afdded01 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete-all.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete-all.tsx @@ -2,6 +2,7 @@ import { href } from 'react-router'; import * as models from '~/models'; import * as requestOperations from '~/models/helpers/request-operations'; +import { isMcpRequestId } from '~/models/mcp-request'; import { isSocketIORequestId } from '~/models/socket-io-request'; import { isWebSocketRequestId } from '~/models/websocket-request'; import { invariant } from '~/utils/invariant'; @@ -22,6 +23,8 @@ export async function clientAction({ params }: Route.ClientActionArgs) { await models.webSocketResponse.removeForRequest(requestId, workspaceMeta.activeEnvironmentId); } else if (isSocketIORequestId(requestId)) { await models.socketIOResponse.removeForRequest(requestId, workspaceMeta.activeEnvironmentId); + } else if (isMcpRequestId(requestId)) { + await models.mcpResponse.removeForRequest(requestId, workspaceMeta.activeEnvironmentId); } else { await models.response.removeForRequest(requestId, workspaceMeta.activeEnvironmentId); } diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete.tsx index fee7461d92..122f4f0f57 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete.tsx @@ -2,8 +2,13 @@ import { href } from 'react-router'; import * as models from '~/models'; import * as requestOperations from '~/models/helpers/request-operations'; +import { isMcpRequestId } from '~/models/mcp-request'; +import type { McpResponse } from '~/models/mcp-response'; +import type { Response } from '~/models/response'; import { isSocketIORequestId } from '~/models/socket-io-request'; +import type { SocketIOResponse } from '~/models/socket-io-response'; import { isWebSocketRequestId } from '~/models/websocket-request'; +import type { WebSocketResponse } from '~/models/websocket-response'; import { invariant } from '~/utils/invariant'; import { createFetcherSubmitHook } from '~/utils/router'; @@ -20,36 +25,42 @@ export async function clientAction({ request, params }: Route.ClientActionArgs) const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId); invariant(workspaceMeta, 'Active workspace meta not found'); + const isWebSocketRequest = isWebSocketRequestId(requestId); + const isSocketIORequest = isSocketIORequestId(requestId); + const isMcpRequest = isMcpRequestId(requestId); - if (isWebSocketRequestId(requestId)) { - const res = await models.webSocketResponse.getById(responseId); - invariant(res, 'Response not found'); - await models.webSocketResponse.remove(res); - const response = await models.webSocketResponse.getLatestForRequestId(requestId, workspaceMeta.activeEnvironmentId); - if (response?.requestVersionId) { - await models.requestVersion.restore(response.requestVersionId); - } - await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: response?._id || null }); - } else if (isSocketIORequestId(requestId)) { - const res = await models.socketIOResponse.getById(responseId); - invariant(res, 'Response not found'); - await models.socketIOResponse.remove(res); - const response = await models.socketIOResponse.getLatestForRequestId(requestId, workspaceMeta.activeEnvironmentId); - if (response?.requestVersionId) { - await models.requestVersion.restore(response.requestVersionId); - } - await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: response?._id || null }); + let responseModel; + if (isWebSocketRequest) { + responseModel = models.webSocketResponse; + } else if (isSocketIORequest) { + responseModel = models.socketIOResponse; + } else if (isMcpRequest) { + responseModel = models.mcpResponse; } else { - const res = await models.response.getById(responseId); - invariant(res, 'Response not found'); - await models.response.remove(res); - const response = await models.response.getLatestForRequestId(requestId, workspaceMeta.activeEnvironmentId); - if (response?.requestVersionId) { - await models.requestVersion.restore(response.requestVersionId); - } - await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: response?._id || null }); + responseModel = models.response; } + const res = await responseModel.getById(responseId); + invariant(res, 'Response not found'); + + // Type-safe remove operation based on the request type + if (isWebSocketRequest) { + await models.webSocketResponse.remove(res as WebSocketResponse); + } else if (isSocketIORequest) { + await models.socketIOResponse.remove(res as SocketIOResponse); + } else if (isMcpRequest) { + await models.mcpResponse.remove(res as McpResponse); + } else { + await models.response.remove(res as Response); + } + const response = await responseModel.getLatestForRequestId(requestId, workspaceMeta.activeEnvironmentId); + if (response?.requestVersionId) { + await models.requestVersion.restore(response.requestVersionId); + } + await models.requestMeta.updateOrCreateByParentId(requestId, { + activeResponseId: response?._id || null, + }); + return null; } diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId.connect.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId.connect.tsx index e5c29a25f3..c7dac15d48 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId.connect.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId.connect.tsx @@ -31,6 +31,7 @@ export async function clientAction({ params, request }: Route.ClientActionArgs) window.main.mcp.connect({ requestId, + workspaceId, transportType: rendered.transportType || 'streamable-http', url: rendered.url, headers: rendered.headers, diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.tsx index ab9fa01d0d..3f4d7868ca 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.tsx @@ -1,4 +1,3 @@ -import type { Prompt, Resource, ResourceTemplate, Tool } from '@modelcontextprotocol/sdk/types.js'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useCallback, useEffect, useRef, useState } from 'react'; import { @@ -18,17 +17,35 @@ import { NavLink, redirect, useParams } from 'react-router'; import { useLocalStorage } from 'react-use'; import { DEFAULT_SIDEBAR_SIZE } from '~/common/constants'; -import type { McpServerData } from '~/main/network/mcp'; +import { + getDefaultServerCapabilities, + type McpServerData, + METHOD_INITIALIZE, + METHOD_LIST_PROMPTS, + METHOD_LIST_RESOURCE_TEMPLATES, + METHOD_LIST_RESOURCES, + METHOD_LIST_TOOLS, +} from '~/common/mcp-utils'; +import type { McpEvent, McpMessageEvent } from '~/main/network/mcp'; import * as models from '~/models'; -import type { McpServerPrimitiveTypes } from '~/models/mcp-request'; +import type { McpRequest, McpServerPrimitiveTypes } from '~/models/mcp-request'; import { useRootLoaderData } from '~/root'; import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; import { useMcpRequestLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId'; +import { McpActionsDropdown } from '~/ui/components/dropdowns/mcp-actions-dropdown'; import { WorkspaceDropdown } from '~/ui/components/dropdowns/workspace-dropdown'; import { EnvironmentPicker } from '~/ui/components/environment-picker'; import { ErrorBoundary } from '~/ui/components/error-boundary'; import { Icon } from '~/ui/components/icon'; import { McpRequestPane } from '~/ui/components/mcp/mcp-request-pane'; +import { + type PrimitiveSubItem, + type PrimitiveTypeItem, + type PromptItem, + type ResourceItem, + type ResourceTemplateItem, + type ToolItem, +} from '~/ui/components/mcp/types'; import { WorkspaceEnvironmentsEditModal } from '~/ui/components/modals/workspace-environments-edit-modal'; import { OrganizationTabList } from '~/ui/components/tabs/tab-list'; import { McpRealtimeResponsePane } from '~/ui/components/websockets/realtime-response-pane'; @@ -38,20 +55,6 @@ import { invariant } from '~/utils/invariant'; import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp'; -interface CommonItemProps { - itemLevel: number; - hide: boolean; -} -type ToolItem = Tool & { type: 'tools' } & CommonItemProps; -type ResourceItem = Resource & { type: 'resources' } & CommonItemProps; -type ResourceTemplateItem = ResourceTemplate & { type: 'resources' } & CommonItemProps; -type PromptItem = Prompt & { type: 'prompts' } & CommonItemProps; -export type PrimitiveSubItemTypes = ToolItem | ResourceItem | ResourceTemplateItem | PromptItem; -interface PrimitiveTypeItem extends CommonItemProps { - type: McpServerPrimitiveTypes; - name: string; -} - export async function clientLoader({ params }: Route.ClientLoaderArgs) { if (!params.requestId) { const { projectId, workspaceId, organizationId } = params; @@ -78,7 +81,7 @@ const McpPage = () => { projectId: string; workspaceId: string; }; - const { activeRequest } = useMcpRequestLoaderData()!; + const { activeRequest, activeResponse } = useMcpRequestLoaderData()!; const sidebarPanelRef = useRef(null); const [isEnvironmentPickerOpen, setIsEnvironmentPickerOpen] = useState(false); const [isEnvironmentModalOpen, setEnvironmentModalOpen] = useState(false); @@ -87,10 +90,10 @@ const McpPage = () => { const { settings } = useRootLoaderData()!; const [mcpServerData, setMcpServerData] = useState(null); const [collapsedPrimitives, setCollapsedPrimitives] = useState([]); - const [selectedPrimitiveItem, setSelectedPrimitiveItem] = useState(null); - console.log('selectedPrimitiveItem', selectedPrimitiveItem); + const [selectedPrimitiveItem, setSelectedPrimitiveItem] = useState(null); + const getPrimitiveCollection = () => { - const collection: (PrimitiveTypeItem | PrimitiveSubItemTypes)[] = []; + const collection: (PrimitiveTypeItem | PrimitiveSubItem)[] = []; if (mcpServerData) { const { primitives } = mcpServerData; const { tools, resources, resourceTemplates, prompts } = primitives; @@ -140,21 +143,7 @@ const McpPage = () => { }; const getServerCapabilities = () => { - const serverCapabilities = { - tools: { - enabled: true, - listChanged: false, - }, - resources: { - enabled: true, - listChanged: false, - subscribe: true, - }, - prompts: { - enabled: true, - listChanged: false, - }, - }; + const serverCapabilities = getDefaultServerCapabilities(); if (mcpServerData) { const { tools, resources, prompts } = mcpServerData.serverCapabilities; if (tools) { @@ -183,10 +172,9 @@ const McpPage = () => { serverCapabilities.resources.listChanged || serverCapabilities.prompts.listChanged; // TODO Use these variables to enable notification - console.log('enableNotification', enableNotification); - console.log('allowSubscribeResources', allowSubscribeResources); - // TODO Use this for showing details - console.log('selectedPrimitiveItem', selectedPrimitiveItem); + console.log(`enableNotification`, enableNotification); + console.log(`allowSubscribeResources`, allowSubscribeResources); + const requestId = activeRequest._id; const { activeEnvironment } = useWorkspaceLoaderData()!; const readyState = useReadyState({ requestId, protocol: 'mcp' }); @@ -251,16 +239,42 @@ const McpPage = () => { useEffect(() => { const updateServerData = async () => { - const serverData = await window.main.mcp.getServerData({ requestId }); - setMcpServerData(serverData!); + const findFirstMatchEventData = (mcpEvents: McpEvent[], method: string) => { + const firstMatchEvent = mcpEvents.find( + event => 'method' in event && event.method === method, + ) as McpMessageEvent; + if (firstMatchEvent) { + return firstMatchEvent.data.result; + } + return undefined; + }; + const activeResponseId = activeResponse?._id; + if (activeResponseId) { + const allEvents = await window.main.mcp.event.findMany({ responseId: activeResponseId }); + const serverCapabilities = + findFirstMatchEventData(allEvents, METHOD_INITIALIZE)?.capabilities || getDefaultServerCapabilities(); + const tools = findFirstMatchEventData(allEvents, METHOD_LIST_TOOLS)?.tools || []; + const resources = findFirstMatchEventData(allEvents, METHOD_LIST_RESOURCES)?.resources || []; + const resourceTemplates = + findFirstMatchEventData(allEvents, METHOD_LIST_RESOURCE_TEMPLATES)?.resourceTemplates || []; + const prompts = findFirstMatchEventData(allEvents, METHOD_LIST_PROMPTS)?.prompts || []; + const mcpServerData = { + serverCapabilities: serverCapabilities, + primitives: { + tools, + resources, + resourceTemplates, + prompts, + }, + } as McpServerData; + setMcpServerData(mcpServerData); + } }; if (readyState) { // Get MCP server data when connection is ready updateServerData(); - } else { - setMcpServerData(null); } - }, [readyState, requestId]); + }, [readyState, activeResponse?._id]); return ( { // Click a specified primitive const [type, name] = id.split('_'); const item = visibleCollection.find(i => i.itemLevel === 1 && i.type === type && i.name === name); - setSelectedPrimitiveItem(item as PrimitiveSubItemTypes); + setSelectedPrimitiveItem(item as PrimitiveSubItem); } }} > {virtualItem => { const item = visibleCollection[virtualItem.index]; - const label = 'title' in item ? item.title : item.name; - const uniqueId = item.itemLevel === 0 ? `root_${item.type}` : `${item.type}_${item.name}`; - const itemLevel = item.itemLevel; return ( - -
-
- {itemLevel === 0 && ( - - )} - {item.type === 'tools' && item.itemLevel === 1 && ( - - Tool - - )} - {item.type === 'resources' && item.itemLevel === 1 && ( - - Res - - )} - {item.type === 'prompts' && item.itemLevel === 1 && ( - - Prompt - - )} - {label} -
-
-
+ /> ); }} @@ -438,7 +416,9 @@ const McpPage = () => { @@ -454,4 +434,76 @@ const McpPage = () => { ); }; +const CollectionGridListItem = ({ + activeRequest, + style, + item, + collapsedPrimitives, +}: { + activeRequest: McpRequest; + item: PrimitiveTypeItem | PrimitiveSubItem; + style: React.CSSProperties; + collapsedPrimitives: McpServerPrimitiveTypes[]; +}) => { + const label = 'title' in item ? item.title : item.name; + const uniqueId = item.itemLevel === 0 ? `root_${item.type}` : `${item.type}_${item.name}`; + const itemLevel = item.itemLevel; + const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); + const triggerRef = useRef(null); + + return ( + +
{ + e.preventDefault(); + setIsContextMenuOpen(true); + }} + className="relative flex h-[--line-height-xs] w-full select-none items-center gap-2 overflow-hidden pl-4 pr-2 text-[--hl] outline-none transition-colors group-hover:bg-[--hl-xs] group-focus:bg-[--hl-sm] data-[selected=true]:text-[--color-font]" + style={{ + paddingLeft: `${itemLevel}em`, + }} + > +
+ {itemLevel === 0 && ( + + )} + {item.type === 'tools' && item.itemLevel === 1 && ( + + Tool + + )} + {item.type === 'resources' && item.itemLevel === 1 && ( + + Res + + )} + {item.type === 'prompts' && item.itemLevel === 1 && ( + + Prompt + + )} + {label} +
+ +
+
+ ); +}; + export default McpPage; diff --git a/packages/insomnia/src/ui/components/dropdowns/mcp-actions-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/mcp-actions-dropdown.tsx new file mode 100644 index 0000000000..517497d053 --- /dev/null +++ b/packages/insomnia/src/ui/components/dropdowns/mcp-actions-dropdown.tsx @@ -0,0 +1,121 @@ +import type { IconName } from '@fortawesome/fontawesome-svg-core'; +import React from 'react'; +import { Button, Collection, Header, Menu, MenuItem, MenuSection, MenuTrigger, Popover } from 'react-aria-components'; + +import type { PlatformKeyCombinations } from '../../../common/settings'; +import type { McpRequest } from '../../../models/mcp-request'; +import { Icon } from '../icon'; +import type { PrimitiveSubItem, PrimitiveTypeItem } from '../mcp/types'; + +interface Props { + item: PrimitiveTypeItem | PrimitiveSubItem; + request: McpRequest; + triggerRef: React.RefObject; + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +} + +export const McpActionsDropdown = ({ item, request, isOpen, onOpenChange, triggerRef }: Props) => { + const { itemLevel, type } = item; + + if (itemLevel !== 0) { + // Only show for capability type item + return null; + } + + const requestId = request._id; + + const handleRefreshPrimitive = () => { + if (type === 'tools') { + window.main.mcp.primitive.listTools({ requestId }); + } else if (type === 'prompts') { + window.main.mcp.primitive.listPrompts({ requestId }); + } else if (type === 'resources') { + window.main.mcp.primitive.listResources({ requestId }); + } + }; + + const mcpPrimitiveActionList: { + name: string; + id: string; + icon: IconName; + items: { + id: string; + name: string; + icon: IconName; + hint?: PlatformKeyCombinations; + action: () => void; + }[]; + }[] = [ + { + name: 'Actions', + id: 'actions', + icon: 'cog', + items: [ + { + id: 'Refresh', + name: 'Refresh', + action: handleRefreshPrimitive, + icon: 'refresh', + }, + ], + }, + ]; + + return ( + { + onOpenChange(isOpen); + }} + > + + + + mcpPrimitiveActionList + .find(i => i.items.find(a => a.id === key)) + ?.items.find(a => a.id === key) + ?.action() + } + items={mcpPrimitiveActionList} + className="min-w-max select-none overflow-y-auto rounded-md border border-solid border-[--hl-sm] bg-[--color-bg] py-2 text-sm shadow-lg focus:outline-none" + > + {section => ( + +
+ {section.name} +
+ + {item => ( + + + {item.name} + + )} + +
+ )} +
+
+
+ ); +}; diff --git a/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx index afd8d3b59a..8f63c3f82d 100644 --- a/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx @@ -8,17 +8,14 @@ import { useRequestResponseDeleteAllActionFetcher } from '~/routes/organization. import { decompressObject } from '../../../common/misc'; import * as models from '../../../models/index'; +import { isMcpResponse, type McpResponse } from '../../../models/mcp-response'; import { isRequest, type Request } from '../../../models/request'; +import { type RequestVersion } from '../../../models/request-version'; import type { Response } from '../../../models/response'; import { isSocketIOResponse, type SocketIOResponse } from '../../../models/socket-io-response'; import type { WebSocketRequest } from '../../../models/websocket-request'; import { isWebSocketResponse, type WebSocketResponse } from '../../../models/websocket-response'; import { useWorkspaceLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; -import { - type RequestLoaderData, - useRequestLoaderData, - type WebSocketRequestLoaderData, -} from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; import { useRequestMetaPatcher } from '../../hooks/use-request'; import { Dropdown, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown'; import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; @@ -28,10 +25,16 @@ import { TimeTag } from '../tags/time-tag'; import { URLTag } from '../tags/url-tag'; import { TimeFromNow } from '../time-from-now'; +type ResponseType = Response | WebSocketResponse | SocketIOResponse | McpResponse; + export const ResponseHistoryDropdown = ({ activeResponse, + responses, + requestVersions, }: { - activeResponse: Response | WebSocketResponse | SocketIOResponse; + activeResponse: ResponseType; + responses: ResponseType[]; + requestVersions: RequestVersion[]; }) => { const { organizationId, projectId, workspaceId, requestId } = useParams() as { organizationId: string; @@ -42,9 +45,9 @@ export const ResponseHistoryDropdown = ({ const dropdownRef = useRef(null); const patchRequestMeta = useRequestMetaPatcher(); const { activeEnvironment } = useWorkspaceLoaderData()!; - const { responses, requestVersions } = useRequestLoaderData() as RequestLoaderData | WebSocketRequestLoaderData; + // const { responses, requestVersions } = useRequestLoaderData() as RequestLoaderData | WebSocketRequestLoaderData; const now = new Date(); - const categories: Record = { + const categories: Record = { minutes: [], hours: [], today: [], @@ -56,7 +59,7 @@ export const ResponseHistoryDropdown = ({ const deleteAllReponsesFetcher = useRequestResponseDeleteAllActionFetcher(); const handleSetActiveResponse = useCallback( - async (requestId: string, activeResponse: Response | WebSocketResponse) => { + async (requestId: string, activeResponse: ResponseType) => { if (isWebSocketResponse(activeResponse)) { window.main.webSocket.close({ requestId }); } @@ -65,6 +68,10 @@ export const ResponseHistoryDropdown = ({ window.main.socketIO.close({ requestId }); } + if (isMcpResponse(activeResponse)) { + window.main.mcp.close({ requestId }); + } + if (activeResponse.requestVersionId) { await models.requestVersion.restore(activeResponse.requestVersionId); } @@ -80,6 +87,8 @@ export const ResponseHistoryDropdown = ({ window.main.webSocket.close({ requestId }); } else if (isSocketIOResponse(activeResponse)) { window.main.socketIO.close({ requestId }); + } else if (isMcpResponse(activeResponse)) { + window.main.socketIO.close({ requestId }); } deleteResponsesSubmit({ organizationId, @@ -96,12 +105,14 @@ export const ResponseHistoryDropdown = ({ window.main.webSocket.close({ requestId }); } else if (isSocketIOResponse(activeResponse)) { window.main.socketIO.close({ requestId }); + } else if (isMcpResponse(activeResponse)) { + window.main.socketIO.close({ requestId }); } } deleteResponseSubmit({ organizationId, projectId, workspaceId, requestId, responseId: activeResponse._id }); }, [activeResponse, deleteResponseSubmit, organizationId, projectId, workspaceId, requestId]); - responses.forEach((response: Response | WebSocketResponse) => { + responses.forEach(response => { const responseTime = new Date(response.created); const match = Object.entries({ @@ -114,7 +125,7 @@ export const ResponseHistoryDropdown = ({ categories[match].push(response); }); - const renderResponseRow = (response: Response | WebSocketResponse) => { + const renderResponseRow = (response: ResponseType) => { const activeResponseId = activeResponse ? activeResponse._id : 'n/a'; const active = response._id === activeResponseId; const requestVersion = requestVersions.find(({ _id }) => _id === response.requestVersionId); @@ -145,7 +156,7 @@ export const ResponseHistoryDropdown = ({ tooltipDelay={1000} /> - {!isWebSocketResponse(response) && !isSocketIOResponse(response) && ( + {!isWebSocketResponse(response) && !isSocketIOResponse(response) && !isMcpResponse(response) && ( { + const { requestId } = useParams() as { requestId: string }; + const raw = JSON.stringify('data' in event ? event.data : ''); + + const handleDownloadResponseBody = useCallback(async () => { + const { canceled, filePath: outputPath } = await window.dialog.showSaveDialog({ + title: 'Save Response Body', + buttonLabel: 'Save', + }); + + if (canceled || !outputPath) { + return; + } + + const to = fs.createWriteStream(outputPath); + + to.on('error', err => { + showError({ + title: 'Save Failed', + message: 'Failed to save response body', + error: err, + }); + }); + + to.write(raw); + + to.end(); + }, [raw]); + + const handleCopyResponseToClipboard = useCallback(() => { + window.clipboard.writeText(raw); + }, [raw]); + + const patchRequestMeta = useRequestMetaPatcher(); + + let pretty = raw; + try { + const parsed = JSON.parse(raw); + pretty = JSON.stringify(parsed, null, '\t'); + } catch { + // Can't parse as JSON. + } + const { activeRequestMeta } = useMcpRequestLoaderData()!; + const previewMode = ('previewMode' in activeRequestMeta && activeRequestMeta.previewMode) || PREVIEW_MODE_SOURCE; + return ( +
+
+ + {getPreviewModeName(previewMode)} + + + } + > + + {PREVIEW_MODES.map(mode => ( + + patchRequestMeta(requestId, { previewMode: mode })} + /> + + ))} + + + + + + + + + + +
+
+ +
+
+ ); +}; + +export const McpEventView = ({ event }: Props) => { + if (event.type === 'message') { + return ; + } + return null; +}; diff --git a/packages/insomnia/src/ui/components/mcp/mcp-request-pane.tsx b/packages/insomnia/src/ui/components/mcp/mcp-request-pane.tsx index 3761fd0eec..0d4598c7d6 100644 --- a/packages/insomnia/src/ui/components/mcp/mcp-request-pane.tsx +++ b/packages/insomnia/src/ui/components/mcp/mcp-request-pane.tsx @@ -5,7 +5,6 @@ import React, { type FC, useEffect, useRef, useState } from 'react'; import { Button, Heading, Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; -import type { PrimitiveSubItemTypes } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp'; import { InsomniaRjsfForm } from '~/ui/components/rjsf'; import { type AuthTypes } from '../../../common/constants'; @@ -18,6 +17,7 @@ import { AuthWrapper } from '../editors/auth/auth-wrapper'; import { readOnlyWebsocketPairs, RequestHeadersEditor } from '../editors/request-headers-editor'; import { Pane } from '../panes/pane'; import { McpUrlActionBar } from './mcp-url-bar'; +import type { PrimitiveSubItem } from './types'; const supportedAuthTypes: AuthTypes[] = ['apikey', 'basic', 'bearer']; @@ -47,7 +47,7 @@ const PaneReadOnlyBanner = () => { interface Props { environment: Environment | null; readyState: boolean; - selectedPrimitiveItem?: PrimitiveSubItemTypes | null; + selectedPrimitiveItem?: PrimitiveSubItem | null; } export const McpRequestPane: FC = ({ environment, readyState, selectedPrimitiveItem }) => { diff --git a/packages/insomnia/src/ui/components/mcp/types.ts b/packages/insomnia/src/ui/components/mcp/types.ts new file mode 100644 index 0000000000..8572d2b68a --- /dev/null +++ b/packages/insomnia/src/ui/components/mcp/types.ts @@ -0,0 +1,18 @@ +import type { Prompt, Resource, ResourceTemplate, Tool } from '@modelcontextprotocol/sdk/types.js'; + +import type { McpServerPrimitiveTypes } from '../../../models/mcp-request'; + +interface CommonItemProps { + itemLevel: number; + hide: boolean; +} + +export type ToolItem = Tool & { type: 'tools' } & CommonItemProps; +export type ResourceItem = Resource & { type: 'resources' } & CommonItemProps; +export type ResourceTemplateItem = ResourceTemplate & { type: 'resources' } & CommonItemProps; +export type PromptItem = Prompt & { type: 'prompts' } & CommonItemProps; +export type PrimitiveSubItem = ToolItem | ResourceItem | ResourceTemplateItem | PromptItem; +export interface PrimitiveTypeItem extends CommonItemProps { + type: McpServerPrimitiveTypes; + name: string; +} diff --git a/packages/insomnia/src/ui/components/panes/response-pane.tsx b/packages/insomnia/src/ui/components/panes/response-pane.tsx index 6b0c72e8a6..be98f47bf3 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/response-pane.tsx @@ -39,7 +39,8 @@ interface Props { activeRequestId: string; } export const ResponsePane: FC = ({ activeRequestId }) => { - const { activeRequest, activeRequestMeta, activeResponse } = useRequestLoaderData() as RequestLoaderData; + const { activeRequest, activeRequestMeta, activeResponse, responses, requestVersions } = + useRequestLoaderData() as RequestLoaderData; const filterHistory = activeRequestMeta.responseFilterHistory || []; const filter = activeRequestMeta.responseFilter || ''; const patchRequestMeta = useRequestMetaPatcher(); @@ -161,7 +162,11 @@ export const ResponsePane: FC = ({ activeRequestId }) => { - + )} diff --git a/packages/insomnia/src/ui/components/websockets/event-log-view.tsx b/packages/insomnia/src/ui/components/websockets/event-log-view.tsx index 7888608886..bb17560afd 100644 --- a/packages/insomnia/src/ui/components/websockets/event-log-view.tsx +++ b/packages/insomnia/src/ui/components/websockets/event-log-view.tsx @@ -4,26 +4,30 @@ import React, { type FC, useEffect, useRef } from 'react'; import { Cell, Column, Row, Table, TableBody, TableHeader } from 'react-aria-components'; import type { CurlEvent } from '../../../main/network/curl'; +import type { McpEvent } from '../../../main/network/mcp'; import type { SocketIOEvent } from '../../../main/network/socket-io'; import type { WebSocketEvent } from '../../../main/network/websocket'; import { type IconId, SvgIcon } from '../svg-icon'; +type EventTypes = WebSocketEvent | CurlEvent | SocketIOEvent | McpEvent; const Timestamp: FC<{ time: Date | number }> = ({ time }) => { const date = format(time, 'HH:mm:ss'); return <>{date}; }; interface Props { - events: (WebSocketEvent | CurlEvent | SocketIOEvent)[]; + events: EventTypes[]; selectionId?: string; - onSelect: (event: WebSocketEvent | CurlEvent | SocketIOEvent) => void; + onSelect: (event: EventTypes) => void; } -const isSocketIOEvent = (event: WebSocketEvent | CurlEvent | SocketIOEvent): event is SocketIOEvent => { +const isSocketIOEvent = (event: EventTypes): event is SocketIOEvent => { return 'eventName' in event && typeof event.eventName === 'string'; }; -function getIcon(event: WebSocketEvent | CurlEvent | SocketIOEvent): IconId { +const isMcpEvent = (event: EventTypes): event is McpEvent => event._id.toString().startsWith('mcp-'); + +function getIcon(event: EventTypes): IconId { switch (event.type) { case 'message': { if (event.direction === 'OUTGOING') { @@ -55,7 +59,7 @@ function getIcon(event: WebSocketEvent | CurlEvent | SocketIOEvent): IconId { } } -const getMessage = (event: WebSocketEvent | CurlEvent | SocketIOEvent): string | JSX.Element => { +const getMessage = (event: EventTypes): string | JSX.Element => { switch (event.type) { case 'message': { if (isSocketIOEvent(event)) { @@ -71,6 +75,11 @@ const getMessage = (event: WebSocketEvent | CurlEvent | SocketIOEvent): string | ); } + if (isMcpEvent(event)) { + if ('method' in event) { + return
{event.method}
; + } + } if ('data' in event && typeof event.data === 'object') { return 'Binary data'; } diff --git a/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx b/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx index f072a210df..069cc02bb6 100644 --- a/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx +++ b/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx @@ -4,22 +4,26 @@ import React, { type FC, useEffect, useMemo, useState } from 'react'; import { Button, Input, SearchField, Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; -import { useMcpRequestLoaderData } from '../../..//routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId'; import { getSetCookieHeaders } from '../../../common/misc'; import type { CurlEvent } from '../../../main/network/curl'; import type { ResponseTimelineEntry } from '../../../main/network/libcurl-promise'; +import type { McpEvent } from '../../../main/network/mcp'; import type { SocketIOEvent } from '../../../main/network/socket-io'; import type { WebSocketEvent } from '../../../main/network/websocket'; +import { isMcpResponse, type McpResponse } from '../../../models/mcp-response'; +import type { RequestVersion } from '../../../models/request-version'; import type { Response } from '../../../models/response'; import { isSocketIOResponse, type SocketIOResponse } from '../../../models/socket-io-response'; -import type { WebSocketResponse } from '../../../models/websocket-response'; +import { type WebSocketResponse } from '../../../models/websocket-response'; import { useRequestLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { useMcpRequestLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId'; import { deserializeNDJSON } from '../../../utils/ndjson'; import { useReadyState } from '../../hooks/use-ready-state'; import { useRealtimeConnectionEvents } from '../../hooks/use-realtime-connection-events'; import { ResponseHistoryDropdown } from '../dropdowns/response-history-dropdown'; import { ErrorBoundary } from '../error-boundary'; import { Icon } from '../icon'; +import { McpEventView } from '../mcp/event-view'; import { Pane, PaneHeader } from '../panes/pane'; import { PlaceholderResponsePane } from '../panes/placeholder-response-pane'; import { SocketIOEventView } from '../socket-io/event-view'; @@ -35,7 +39,7 @@ import { EventLogView } from './event-log-view'; import { EventView } from './event-view'; export const RealtimeResponsePane: FC<{ requestId: string }> = () => { - const { activeResponse } = useRequestLoaderData()!; + const { activeResponse, responses, requestVersions } = useRequestLoaderData()!; if (!activeResponse) { return ( @@ -45,11 +49,13 @@ export const RealtimeResponsePane: FC<{ requestId: string }> = () => { ); } - return ; + return ( + + ); }; export const McpRealtimeResponsePane = () => { - const { activeResponse } = useMcpRequestLoaderData()!; + const { activeResponse, responses, requestVersions } = useMcpRequestLoaderData()!; if (!activeResponse) { return ( @@ -59,13 +65,19 @@ export const McpRealtimeResponsePane = () => { ); } - return ; + return ( + + ); }; +type ResponseType = WebSocketResponse | Response | SocketIOResponse | McpResponse; +type EventType = CurlEvent | WebSocketEvent | SocketIOEvent | McpEvent; const RealtimeActiveResponsePane: FC<{ - response: WebSocketResponse | Response | SocketIOResponse; -}> = ({ response }) => { - const [selectedEvent, setSelectedEvent] = useState(null); + response: ResponseType; + responses: ResponseType[]; + requestVersions: RequestVersion[]; +}> = ({ response, responses, requestVersions }) => { + const [selectedEvent, setSelectedEvent] = useState(null); const [timeline, setTimeline] = useState([]); const [clearEventsBefore, setClearEventsBefore] = useState(null); const [searchQuery, setSearchQuery] = useState(''); @@ -75,20 +87,26 @@ const RealtimeActiveResponsePane: FC<{ if (isSocketIOResponse(response)) { return 'socketIO'; } + if (isMcpResponse(response)) { + return 'mcp'; + } return response.type === 'WebSocketResponse' ? 'webSocket' : 'curl'; }, [response]); - const allEvents = useRealtimeConnectionEvents({ responseId: response._id, protocol }) as ( - | CurlEvent - | WebSocketEvent - | SocketIOEvent - )[]; + const allEvents = useRealtimeConnectionEvents({ responseId: response._id, protocol }) as EventType[]; const requestId = response.parentId; const readyState = useReadyState({ requestId: requestId, protocol }); - const handleSelection = (event: CurlEvent | WebSocketEvent | SocketIOEvent) => { - setSelectedEvent((selected: CurlEvent | WebSocketEvent | SocketIOEvent | null) => - selected?._id === event._id ? null : event, - ); + const handleSelection = (event: EventType) => { + setSelectedEvent((selected: EventType | null) => (selected?._id === event._id ? null : event)); + }; + const getEventView = (selectedEvent: EventType) => { + if (isSocketIOResponse(response)) { + return ; + } else if (isMcpResponse(response)) { + return ; + } + + return ; }; const events = useMemo( @@ -157,7 +175,8 @@ const RealtimeActiveResponsePane: FC<{ }; }, [response.timelinePath, events.length]); - const cookieHeaders = !isSocketIOResponse(response) ? getSetCookieHeaders(response.headers) : []; + const cookieHeaders = + !isSocketIOResponse(response) && !isMcpResponse(response) ? getSetCookieHeaders(response.headers) : []; return ( @@ -174,7 +193,7 @@ const RealtimeActiveResponsePane: FC<{ )} - + -
- {isSocketIOResponse(response) ? ( - - ) : ( - - )} -
+
{getEventView(selectedEvent)}
)} diff --git a/packages/insomnia/src/ui/hooks/use-realtime-connection-events.ts b/packages/insomnia/src/ui/hooks/use-realtime-connection-events.ts index 082825ec24..9796590034 100644 --- a/packages/insomnia/src/ui/hooks/use-realtime-connection-events.ts +++ b/packages/insomnia/src/ui/hooks/use-realtime-connection-events.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import type { CurlEvent } from '../../main/network/curl'; +import type { McpEvent } from '../../main/network/mcp'; import type { SocketIOEvent } from '../../main/network/socket-io'; import type { WebSocketEvent } from '../../main/network/websocket'; @@ -11,7 +12,7 @@ export function useRealtimeConnectionEvents({ responseId: string; protocol: 'curl' | 'webSocket' | 'socketIO' | 'mcp'; }) { - const [events, setEvents] = useState([]); + const [events, setEvents] = useState([]); const updateEvents = useCallback(async () => { const allEvents = await window.main[protocol].event.findMany({ responseId }); setEvents(allEvents);