From 3ee71bed8844a0ff2163da2046712b00471ba34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Tue, 4 Nov 2025 10:01:39 -0300 Subject: [PATCH 01/20] app: Add langchain/mcp-adapters 1.0.0 There's supposed to be an AT in front of langchain package name but it's not there because k8s bot. Co-Authored-By: Ashu Ghildiyal --- app/package-lock.json | 2084 +++++++++++++++++++++++++++++++++++++++-- app/package.json | 1 + 2 files changed, 1997 insertions(+), 88 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index c80fdd86ab4..519a04ef6c7 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,6 +8,7 @@ "name": "headlamp", "version": "0.37.0", "dependencies": { + "@langchain/mcp-adapters": "^1.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "dotenv": "^16.4.5", @@ -1958,6 +1959,13 @@ "hasInstallScript": true, "optional": true }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "license": "MIT", + "peer": true + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -3495,6 +3503,167 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@langchain/core": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.0.3.tgz", + "integrity": "sha512-3ABwsHfpvsDzNWZYAgPist5q+qjiU7lZDzEk7A9rXVhdhksPH8aFWnwFD6JhNP4wCgVqHWxvBaC1pj2gMa7iTg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.3.64", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@langchain/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@langchain/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/langgraph": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.0.1.tgz", + "integrity": "sha512-7y8OTDLrHrpJ55Y5x7c7zU2BbqNllXwxM106Xrd+NaQB5CpEb4hbUfIwe4XmhhscKPwvhXAq3tjeUxw9MCiurQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@langchain/langgraph-checkpoint": "^1.0.0", + "@langchain/langgraph-sdk": "~1.0.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.0.1", + "zod": "^3.25.32 || ^4.1.0", + "zod-to-json-schema": "^3.x" + }, + "peerDependenciesMeta": { + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-checkpoint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz", + "integrity": "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==", + "license": "MIT", + "peer": true, + "dependencies": { + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.0.1" + } + }, + "node_modules/@langchain/langgraph-sdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.0.0.tgz", + "integrity": "sha512-g25ti2W7Dl5wUPlNK+0uIGbeNFqf98imhHlbdVVKTTkDYLhi/pI1KTgsSSkzkeLuBIfvt2b0q6anQwCs7XBlbw==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^9.0.0" + }, + "peerDependencies": { + "@langchain/core": "^1.0.1", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/mcp-adapters": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/mcp-adapters/-/mcp-adapters-1.0.0.tgz", + "integrity": "sha512-bvoCF1F8iKsS4AaHPimhvzLX3Zpel7+nTSecUIDQHXQ6G1+rpvbx2zFJYRvhjLrbjvFneRN+HO0XAHtUuzWJdA==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.18.2", + "debug": "^4.4.3", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20.10.0" + }, + "optionalDependencies": { + "extended-eventsource": "^1.7.0" + }, + "peerDependencies": { + "@langchain/core": "^1.0.0", + "@langchain/langgraph": "^1.0.0" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": false + }, + "@langchain/langgraph": { + "optional": false + } + } + }, "node_modules/@malept/cross-spawn-promise": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", @@ -3567,6 +3736,60 @@ "node": ">= 10.0.0" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.21.0.tgz", + "integrity": "sha512-YFBsXJMFCyI1zP98u7gezMFKX4lgu/XpoZJk7ufI6UlFKXLj2hAMUuRlQX/nrmIPOmhRrG6tw2OQ2ZA/ZlXYpQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@mswjs/interceptors": { "version": "0.38.7", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz", @@ -3857,6 +4080,13 @@ "@types/node": "*" } }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT", + "peer": true + }, "node_modules/@types/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", @@ -3876,6 +4106,13 @@ "integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==", "dev": true }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT", + "peer": true + }, "node_modules/@types/verror": { "version": "1.10.9", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz", @@ -4138,6 +4375,40 @@ "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -4187,6 +4458,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -5031,7 +5341,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5099,6 +5408,26 @@ "bluebird": "^3.5.5" } }, + "node_modules/body-parser": { + "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==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -5382,6 +5711,15 @@ "node": ">= 10.0.0" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -5406,7 +5744,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5416,6 +5753,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5784,12 +6137,81 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/console-table-printer": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz", + "integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==", + "license": "MIT", + "peer": true, + "dependencies": { + "simple-wcswidth": "^1.1.2" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/copyfiles": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", @@ -5901,6 +6323,19 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -6124,11 +6559,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6139,6 +6575,16 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -6287,6 +6733,15 @@ "node": ">=0.4.0" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -6510,7 +6965,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6526,6 +6980,12 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -6812,6 +7272,15 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", @@ -6939,7 +7408,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6949,7 +7417,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -7012,7 +7479,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7121,6 +7587,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -7697,6 +8169,43 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT", + "peer": true + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7757,6 +8266,91 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "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==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extended-eventsource": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extended-eventsource/-/extended-eventsource-1.7.0.tgz", + "integrity": "sha512-s8rtvZuYcKBpzytHb5g95cHbZ1J99WeMnV18oKc5wKoxkHzlzpPc/bNAm7Da2Db0BDw0CAu1z3LpH+7UsyzIpw==", + "license": "MIT", + "optional": true + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -7790,8 +8384,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-fifo": { "version": "1.3.2", @@ -7826,6 +8419,22 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -7905,6 +8514,23 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-process": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz", @@ -8012,6 +8638,24 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -8182,7 +8826,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -8216,7 +8859,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -8420,7 +9062,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8506,7 +9147,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8625,6 +9265,31 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8803,7 +9468,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -8931,6 +9595,15 @@ "node": ">= 0.10" } }, + "node_modules/ipaddr.js": { + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -9265,6 +9938,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -10224,6 +10903,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.5.1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10347,6 +11036,42 @@ "node": ">=6" } }, + "node_modules/langsmith": { + "version": "0.3.78", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.78.tgz", + "integrity": "sha512-PVrog/DiTsiyOQ38GeZEIVadgk55/dfE3axagQksT3dt6KhFuRxhNaZrC0rp3dNW9RQJCm/c3tn+PiybwQNY0Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + } + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -10604,12 +11329,32 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -10828,9 +11573,20 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "peer": true, + "bin": { + "mustache": "bin/mustache" + } }, "node_modules/natural-compare": { "version": "1.4.0", @@ -10838,6 +11594,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -10955,17 +11720,15 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, - "peer": true, + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -11085,6 +11848,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -11180,6 +11955,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -11264,6 +12093,15 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -11323,6 +12161,16 @@ "node": "14 || >=16.14" } }, + "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==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -11361,7 +12209,16 @@ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, "engines": { - "node": ">= 6" + "node": ">= 6" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" } }, "node_modules/pkg-dir": { @@ -11534,6 +12391,19 @@ "node": ">= 8" } }, + "node_modules/proxy-addr": { + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -11568,6 +12438,21 @@ } ] }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11649,6 +12534,46 @@ "rimraf": "bin.js" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "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==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/rcedit": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-4.0.1.tgz", @@ -11915,6 +12840,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -12047,6 +12981,22 @@ "node": ">=8.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -12130,8 +13080,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sanitize-filename": { "version": "1.6.3", @@ -12149,9 +13098,10 @@ "dev": true }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -12166,6 +13116,49 @@ "dev": true, "optional": true }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -12182,6 +13175,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12216,6 +13224,12 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12376,16 +13390,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "peer": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -12411,6 +13478,13 @@ "node": ">=10" } }, + "node_modules/simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "license": "MIT", + "peer": true + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -12513,6 +13587,15 @@ "node": ">= 6" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -13013,6 +14096,15 @@ "node": ">=10.13.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -13104,6 +14196,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -13289,6 +14416,15 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -13347,6 +14483,20 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -13370,6 +14520,15 @@ "node": ">= 10.13.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -13874,6 +15033,24 @@ "engines": { "node": ">= 6" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "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==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } }, "dependencies": { @@ -15209,6 +16386,12 @@ "integrity": "sha512-iTZ8cVGZ5dglNRyFdSj8U60mHIrC8XNIuOHN/NkM5/dQP4nsmpyqeQTAADLLQgoFCNJD+DiwQCv8dR2cCeWP4g==", "optional": true }, + "@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "peer": true + }, "@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -16205,6 +17388,89 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@langchain/core": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.0.3.tgz", + "integrity": "sha512-3ABwsHfpvsDzNWZYAgPist5q+qjiU7lZDzEk7A9rXVhdhksPH8aFWnwFD6JhNP4wCgVqHWxvBaC1pj2gMa7iTg==", + "peer": true, + "requires": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.3.64", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.25.76 || ^4" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "peer": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "peer": true + } + } + }, + "@langchain/langgraph": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.0.1.tgz", + "integrity": "sha512-7y8OTDLrHrpJ55Y5x7c7zU2BbqNllXwxM106Xrd+NaQB5CpEb4hbUfIwe4XmhhscKPwvhXAq3tjeUxw9MCiurQ==", + "peer": true, + "requires": { + "@langchain/langgraph-checkpoint": "^1.0.0", + "@langchain/langgraph-sdk": "~1.0.0", + "uuid": "^10.0.0" + } + }, + "@langchain/langgraph-checkpoint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz", + "integrity": "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==", + "peer": true, + "requires": { + "uuid": "^10.0.0" + } + }, + "@langchain/langgraph-sdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.0.0.tgz", + "integrity": "sha512-g25ti2W7Dl5wUPlNK+0uIGbeNFqf98imhHlbdVVKTTkDYLhi/pI1KTgsSSkzkeLuBIfvt2b0q6anQwCs7XBlbw==", + "peer": true, + "requires": { + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^9.0.0" + }, + "dependencies": { + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "peer": true + } + } + }, + "@langchain/mcp-adapters": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/mcp-adapters/-/mcp-adapters-1.0.0.tgz", + "integrity": "sha512-bvoCF1F8iKsS4AaHPimhvzLX3Zpel7+nTSecUIDQHXQ6G1+rpvbx2zFJYRvhjLrbjvFneRN+HO0XAHtUuzWJdA==", + "requires": { + "@modelcontextprotocol/sdk": "^1.18.2", + "debug": "^4.4.3", + "extended-eventsource": "^1.7.0", + "zod": "^3.25.76 || ^4" + } + }, "@malept/cross-spawn-promise": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", @@ -16255,6 +17521,44 @@ } } }, + "@modelcontextprotocol/sdk": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.21.0.tgz", + "integrity": "sha512-YFBsXJMFCyI1zP98u7gezMFKX4lgu/XpoZJk7ufI6UlFKXLj2hAMUuRlQX/nrmIPOmhRrG6tw2OQ2ZA/ZlXYpQ==", + "requires": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "@mswjs/interceptors": { "version": "0.38.7", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz", @@ -16529,6 +17833,12 @@ "@types/node": "*" } }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "peer": true + }, "@types/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", @@ -16547,6 +17857,12 @@ "integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==", "dev": true }, + "@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "peer": true + }, "@types/verror": { "version": "1.10.9", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz", @@ -16716,6 +18032,30 @@ "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true }, + "accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "requires": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, "acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -16750,6 +18090,32 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -17415,8 +18781,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "binary-extensions": { "version": "2.2.0", @@ -17466,6 +18831,22 @@ "bluebird": "^3.5.5" } }, + "body-parser": { + "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==", + "requires": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + } + }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -17678,6 +19059,11 @@ "sax": "^1.2.4" } }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, "call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -17696,12 +19082,20 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "requires": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -17971,12 +19365,51 @@ } } }, + "console-table-printer": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz", + "integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==", + "peer": true, + "requires": { + "simple-wcswidth": "^1.1.2" + } + }, + "content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "requires": { + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" + }, "copyfiles": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", @@ -18063,6 +19496,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -18216,13 +19658,19 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "peer": true + }, "decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -18329,6 +19777,11 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -18495,7 +19948,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -18507,6 +19959,11 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -18740,6 +20197,11 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "encoding-sniffer": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", @@ -18849,14 +20311,12 @@ "es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, "es-get-iterator": { "version": "1.1.3", @@ -18912,7 +20372,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "requires": { "es-errors": "^1.3.0" } @@ -18996,6 +20455,11 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, "escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -19423,6 +20887,30 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "peer": true + }, + "eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "requires": { + "eventsource-parser": "^3.0.1" + } + }, + "eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==" + }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -19467,6 +20955,67 @@ "jest-util": "^29.7.0" } }, + "express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "requires": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, + "express-rate-limit": { + "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==", + "requires": {} + }, + "extended-eventsource": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extended-eventsource/-/extended-eventsource-1.7.0.tgz", + "integrity": "sha512-s8rtvZuYcKBpzytHb5g95cHbZ1J99WeMnV18oKc5wKoxkHzlzpPc/bNAm7Da2Db0BDw0CAu1z3LpH+7UsyzIpw==", + "optional": true + }, "extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -19489,8 +21038,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-fifo": { "version": "1.3.2", @@ -19522,6 +21070,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==" + }, "fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -19594,6 +21147,19 @@ "to-regex-range": "^5.0.1" } }, + "finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "requires": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + } + }, "find-process": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz", @@ -19677,6 +21243,16 @@ "mime-types": "^2.1.12" } }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -19809,7 +21385,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -19833,7 +21408,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "requires": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -19973,8 +21547,7 @@ "gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "graceful-fs": { "version": "4.2.11", @@ -20039,8 +21612,7 @@ "has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { "version": "1.0.2", @@ -20136,6 +21708,25 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "dependencies": { + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + } + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -20261,7 +21852,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } @@ -20343,6 +21933,11 @@ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" }, + "ipaddr.js": { + "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==" + }, "is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -20557,6 +22152,11 @@ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "dev": true }, + "is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -21272,6 +22872,15 @@ } } }, + "js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "peer": true, + "requires": { + "base64-js": "^1.5.1" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -21371,6 +22980,21 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "langsmith": { + "version": "0.3.78", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.78.tgz", + "integrity": "sha512-PVrog/DiTsiyOQ38GeZEIVadgk55/dfE3axagQksT3dt6KhFuRxhNaZrC0rp3dNW9RQJCm/c3tn+PiybwQNY0Q==", + "peer": true, + "requires": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + } + }, "language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -21573,8 +23197,17 @@ "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + }, + "merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" }, "merge-stream": { "version": "2.0.0", @@ -21716,9 +23349,15 @@ "dev": true }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "peer": true }, "natural-compare": { "version": "1.4.0", @@ -21726,6 +23365,11 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -21828,15 +23472,12 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, - "peer": true + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, "object-is": { "version": "1.1.6", @@ -21917,6 +23558,14 @@ "es-object-atoms": "^1.0.0" } }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -21987,6 +23636,43 @@ } } }, + "p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "peer": true, + "requires": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + } + }, + "p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "peer": true, + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "dependencies": { + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "peer": true + } + } + }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "peer": true, + "requires": { + "p-finally": "^1.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -22047,6 +23733,11 @@ "parse5": "^7.0.0" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -22090,6 +23781,11 @@ } } }, + "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==" + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -22119,6 +23815,11 @@ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true }, + "pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==" + }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -22249,6 +23950,15 @@ "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==" }, + "proxy-addr": { + "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==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -22270,6 +23980,14 @@ "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "dev": true }, + "qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "requires": { + "side-channel": "^1.1.0" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -22323,6 +24041,32 @@ } } }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "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==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "rcedit": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-4.0.1.tgz", @@ -22544,6 +24288,11 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -22640,6 +24389,18 @@ "sprintf-js": "^1.1.2" } }, + "router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "requires": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -22696,8 +24457,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sanitize-filename": { "version": "1.6.3", @@ -22715,9 +24475,9 @@ "dev": true }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==" }, "semver-compare": { "version": "1.0.0", @@ -22726,6 +24486,39 @@ "dev": true, "optional": true }, + "send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "requires": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, "serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -22736,6 +24529,17 @@ "type-fest": "^0.13.1" } }, + "serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "requires": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + } + }, "set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -22764,6 +24568,11 @@ "has-property-descriptors": "^1.0.2" } }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -22878,16 +24687,47 @@ } }, "side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "peer": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "requires": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "signal-exit": { @@ -22904,6 +24744,12 @@ "semver": "^7.5.3" } }, + "simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "peer": true + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -22981,6 +24827,11 @@ "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", "dev": true }, + "statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + }, "stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -23380,6 +25231,11 @@ "streamx": "^2.12.5" } }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, "truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -23450,6 +25306,31 @@ "dev": true, "optional": true }, + "type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "requires": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, "typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -23581,6 +25462,11 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, "untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -23616,6 +25502,12 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "peer": true + }, "v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -23633,6 +25525,11 @@ "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", "dev": true }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, "verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -24019,6 +25916,17 @@ } } } + }, + "zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" + }, + "zod-to-json-schema": { + "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==", + "requires": {} } } } diff --git a/app/package.json b/app/package.json index d4f111ac8aa..3615f764dcf 100644 --- a/app/package.json +++ b/app/package.json @@ -171,6 +171,7 @@ "typescript": "5.5.4" }, "dependencies": { + "@langchain/mcp-adapters": "^1.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "dotenv": "^16.4.5", From 60ccce83a4571a80cc2f46bea0fb1dee967c9392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Tue, 4 Nov 2025 10:48:52 -0300 Subject: [PATCH 02/20] app: main: MCPClient: Add stub MCPClient class and hook it up to main - Add some basic tests - Add a ENABLE_MCP at top of main.ts to allow easily disabling MCP Set HEADLAMP_MCP_ENABLE=false to disable MCP features. Co-Authored-By: Ashu Ghildiyal --- app/.gitignore | 2 + app/electron/main.ts | 22 +++++++- app/electron/mcp/MCPClient.test.ts | 81 ++++++++++++++++++++++++++ app/electron/mcp/MCPClient.ts | 91 ++++++++++++++++++++++++++++++ app/package.json | 3 +- 5 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 app/electron/mcp/MCPClient.test.ts create mode 100644 app/electron/mcp/MCPClient.ts diff --git a/app/.gitignore b/app/.gitignore index 79903bc0b8d..bf7c559eeb0 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -13,3 +13,5 @@ electron/windowSize.test.js electron/env-paths.js electron/runCmd.test.js +electron/mcp/MCPClient.js +electron/mcp/MCPClient.test.js diff --git a/app/electron/main.ts b/app/electron/main.ts index 672a678ca2c..4f545e5c311 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -41,6 +41,7 @@ import url from 'url'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import i18n from './i18next.config'; +import MCPClient from './mcp/MCPClient'; import { addToPath, ArtifactHubHeadlampPkg, @@ -58,6 +59,9 @@ if (process.env.HEADLAMP_RUN_SCRIPT) { runScript(); } +// Enabled by default, set HEADLAMP_MCP_ENABLE=false to disable MCP features +const ENABLE_MCP = process.env.HEADLAMP_MCP_ENABLE !== 'false'; + dotenv.config({ path: path.join(process.resourcesPath, '.env') }); const isDev = process.env.ELECTRON_DEV || false; @@ -139,6 +143,7 @@ const shouldCheckForUpdates = process.env.HEADLAMP_CHECK_FOR_UPDATES !== 'false' // make it global so that it doesn't get garbage collected let mainWindow: BrowserWindow | null; +let mcpClient: MCPClient | null = null; /** * `Action` is an interface for an action to be performed by the plugin manager. @@ -1602,6 +1607,12 @@ function startElecron() { if (userPluginBinDirs.length > 0) { addToPath(userPluginBinDirs, 'userPluginBinDirs plugin'); } + + if (ENABLE_MCP) { + mcpClient = new MCPClient(); + await mcpClient.initialize(); + mcpClient.setMainWindow(mainWindow); + } } if (disableGPU) { @@ -1632,12 +1643,21 @@ function startElecron() { app.once('window-all-closed', app.quit); - app.once('before-quit', () => { + app.once('before-quit', async () => { saveZoomFactor(cachedZoom); i18n.off('languageChanged'); if (mainWindow) { mainWindow.removeAllListeners('close'); } + + if (mcpClient) { + try { + await mcpClient.cleanup(); + mcpClient = null; + } catch (err) { + console.error('Failed to clean up mcpClient:', err); + } + } }); } diff --git a/app/electron/mcp/MCPClient.test.ts b/app/electron/mcp/MCPClient.test.ts new file mode 100644 index 00000000000..54db98c53e3 --- /dev/null +++ b/app/electron/mcp/MCPClient.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MCPClient from './MCPClient'; + +describe('MCPClient', () => { + let client: MCPClient; + let infoSpy: jest.Mock; + + beforeEach(() => { + client = new MCPClient(); + // spy on console.info to avoid noisy output and to assert calls + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}) as unknown as jest.Mock; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('throws from handleClustersChange if not initialized', async () => { + await expect(client.handleClustersChange(['cluster-a'])).rejects.toThrow( + 'MCPClient: not initialized' + ); + }); + + it('initialize is idempotent and logs exactly once', async () => { + await client.initialize(); + await client.initialize(); // second call should be a no-op + + expect(infoSpy).toHaveBeenCalledTimes(1); + expect(infoSpy).toHaveBeenCalledWith('MCPClient: initialized'); + }); + + it('handleClustersChange resolves when initialized and logs clusters', async () => { + await client.initialize(); + await expect(client.handleClustersChange(['cluster-1'])).resolves.toBeUndefined(); + + // initialize + clusters change => at least two calls + expect(infoSpy).toHaveBeenCalledWith('MCPClient: clusters changed ->', ['cluster-1']); + }); + + it('setMainWindow accepts a BrowserWindow-like object and cleanup resets state', async () => { + // use a minimal fake to represent BrowserWindow + const fakeWin = { id: 42 } as unknown as Electron.BrowserWindow; + + await client.initialize(); + client.setMainWindow(fakeWin); + + // handleClustersChange should work when initialized + await expect(client.handleClustersChange(['c-x'])).resolves.toBeUndefined(); + + // cleanup should reset initialized and log cleanup + await client.cleanup(); + expect(infoSpy).toHaveBeenCalledWith('MCPClient: cleaned up'); + + // after cleanup, handleClustersChange should again reject as not initialized + await expect(client.handleClustersChange(['after-cleanup'])).rejects.toThrow( + 'MCPClient: not initialized' + ); + }); + + it('cleanup is safe to call when not initialized', async () => { + // no initialize called + await expect(client.cleanup()).resolves.toBeUndefined(); + // no cleanup log should be emitted since it early-returns when not initialized + expect(infoSpy).not.toHaveBeenCalledWith('MCPClient: cleaned up'); + }); +}); diff --git a/app/electron/mcp/MCPClient.ts b/app/electron/mcp/MCPClient.ts new file mode 100644 index 00000000000..6c918e656fd --- /dev/null +++ b/app/electron/mcp/MCPClient.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { BrowserWindow } from 'electron'; + +const DEBUG = true; + +/** + * MCPClient + * + * Lightweight client intended for use in the Electron main process to manage + * minimal MCP (Multi-Cluster Platform) concerns. + * + * Example: + * ```ts + * const mcpClient = new MCPClient(); + * await mcpClient.initialize(); + * mcpClient.setMainWindow(mainWindow); + * await mcpClient.handleClustersChange(['cluster-1']); + * await mcpClient.cleanup(); + * ``` + */ +export default class MCPClient { + private mainWindow: BrowserWindow | null = null; + private initialized = false; + + constructor() {} + + /** + * Initialize the MCP client. + */ + async initialize(): Promise { + if (this.initialized) { + return; + } + this.initialized = true; + if (DEBUG) { + console.info('MCPClient: initialized'); + } + } + + /** + * Set the main BrowserWindow for IPC notifications. + * + * @param win - The main BrowserWindow instance, or null to clear it. + */ + setMainWindow(win: BrowserWindow | null): void { + this.mainWindow = win; + } + + /** + * Handle clusters change notification. + * + * @param clusters - The new active clusters array, or null if none. + */ + async handleClustersChange(clusters: string[] | null): Promise { + if (DEBUG) { + console.info('MCPClient: clusters changed ->', clusters); + } + if (!this.initialized) { + throw new Error('MCPClient: not initialized'); + } + } + + /** + * Clean up resources used by the MCP client. + */ + async cleanup(): Promise { + if (!this.initialized) { + return; + } + this.mainWindow = null; + this.initialized = false; + if (DEBUG) { + console.info('MCPClient: cleaned up'); + } + } +} diff --git a/app/package.json b/app/package.json index 3615f764dcf..a3cdca7d649 100644 --- a/app/package.json +++ b/app/package.json @@ -123,7 +123,8 @@ "electron/windowSize.js", "electron/env-paths.js", "electron/plugin-management.js", - "electron/runCmd.js" + "electron/runCmd.js", + "electron/mcp/MCPClient.js" ], "extraResources": [ { From 8d809a9fbe38ef52b401c5ac0b9e436c642f2b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Tue, 4 Nov 2025 15:16:48 -0300 Subject: [PATCH 03/20] app: MCPToolStateStore: Add MCPToolStateStore for tool state, stats, and persistence Introduce MCPToolStateStore class to track tool enabled/disabled state, usage statistics, input schemas and descriptions, with load/save and helpers to initialize, query, and replace configs. MCPClient.mcpToolState is the MCPToolStateStore The config can be initialized async for fast app loading. Co-Authored-By: Ashu Ghildiyal --- app/.gitignore | 2 + app/electron/main.ts | 3 +- app/electron/mcp/MCPClient.test.ts | 34 +- app/electron/mcp/MCPClient.ts | 12 +- app/electron/mcp/MCPToolStateStore.test.ts | 191 ++++++++++ app/electron/mcp/MCPToolStateStore.ts | 394 +++++++++++++++++++++ app/package.json | 3 +- 7 files changed, 634 insertions(+), 5 deletions(-) create mode 100644 app/electron/mcp/MCPToolStateStore.test.ts create mode 100644 app/electron/mcp/MCPToolStateStore.ts diff --git a/app/.gitignore b/app/.gitignore index bf7c559eeb0..312da5dbfbc 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -15,3 +15,5 @@ electron/env-paths.js electron/runCmd.test.js electron/mcp/MCPClient.js electron/mcp/MCPClient.test.js +electron/mcp/MCPToolStateStore.js +electron/mcp/MCPToolStateStore.test.js diff --git a/app/electron/main.ts b/app/electron/main.ts index 4f545e5c311..dc80acefa29 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -1609,7 +1609,8 @@ function startElecron() { } if (ENABLE_MCP) { - mcpClient = new MCPClient(); + const configPath = path.join(app.getPath('userData'), 'mcp-tools-config.json'); + mcpClient = new MCPClient(configPath); await mcpClient.initialize(); mcpClient.setMainWindow(mainWindow); } diff --git a/app/electron/mcp/MCPClient.test.ts b/app/electron/mcp/MCPClient.test.ts index 54db98c53e3..6258979649b 100644 --- a/app/electron/mcp/MCPClient.test.ts +++ b/app/electron/mcp/MCPClient.test.ts @@ -14,14 +14,40 @@ * limitations under the License. */ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; import MCPClient from './MCPClient'; +function tmpPath(): string { + return path.join(os.tmpdir(), `mcp-test-${Date.now()}-${Math.random()}.json`); +} + describe('MCPClient', () => { let client: MCPClient; let infoSpy: jest.Mock; + let cfgPath: string; + beforeEach(() => { - client = new MCPClient(); + cfgPath = tmpPath(); + try { + if (fs.existsSync(cfgPath)) fs.unlinkSync(cfgPath); + } catch { + // ignore + } + }); + + afterEach(() => { + try { + if (fs.existsSync(cfgPath)) fs.unlinkSync(cfgPath); + } catch { + // ignore + } + }); + + beforeEach(() => { + client = new MCPClient(cfgPath); // spy on console.info to avoid noisy output and to assert calls infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}) as unknown as jest.Mock; }); @@ -44,6 +70,12 @@ describe('MCPClient', () => { expect(infoSpy).toHaveBeenCalledWith('MCPClient: initialized'); }); + it('config is set after initialize', async () => { + expect((client as any).mcpToolState).toBeNull(); + await client.initialize(); + expect((client as any).mcpToolState).not.toBeNull(); + }); + it('handleClustersChange resolves when initialized and logs clusters', async () => { await client.initialize(); await expect(client.handleClustersChange(['cluster-1'])).resolves.toBeUndefined(); diff --git a/app/electron/mcp/MCPClient.ts b/app/electron/mcp/MCPClient.ts index 6c918e656fd..0f1a9a0141b 100644 --- a/app/electron/mcp/MCPClient.ts +++ b/app/electron/mcp/MCPClient.ts @@ -15,6 +15,7 @@ */ import type { BrowserWindow } from 'electron'; +import { MCPToolStateStore } from './MCPToolStateStore'; const DEBUG = true; @@ -26,7 +27,8 @@ const DEBUG = true; * * Example: * ```ts - * const mcpClient = new MCPClient(); + * const configPath = path.join(app.getPath('userData'), 'mcp-tools-config.json'); + * const mcpClient = new MCPClient(configPath); * await mcpClient.initialize(); * mcpClient.setMainWindow(mainWindow); * await mcpClient.handleClustersChange(['cluster-1']); @@ -36,8 +38,12 @@ const DEBUG = true; export default class MCPClient { private mainWindow: BrowserWindow | null = null; private initialized = false; + private mcpToolState: MCPToolStateStore | null = null; + private readonly configPath: string; - constructor() {} + constructor(configPath: string) { + this.configPath = configPath; + } /** * Initialize the MCP client. @@ -46,6 +52,8 @@ export default class MCPClient { if (this.initialized) { return; } + this.mcpToolState = new MCPToolStateStore(this.configPath); + this.initialized = true; if (DEBUG) { console.info('MCPClient: initialized'); diff --git a/app/electron/mcp/MCPToolStateStore.test.ts b/app/electron/mcp/MCPToolStateStore.test.ts new file mode 100644 index 00000000000..23268df5594 --- /dev/null +++ b/app/electron/mcp/MCPToolStateStore.test.ts @@ -0,0 +1,191 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { MCPToolStateStore } from './MCPToolStateStore'; + +function tmpPath(): string { + return path.join(os.tmpdir(), `mcp-test-${Date.now()}-${Math.random()}.json`); +} + +describe('MCPConfig', () => { + let toolStatePath: string; + + beforeEach(() => { + toolStatePath = tmpPath(); + try { + if (fs.existsSync(toolStatePath)) fs.unlinkSync(toolStatePath); + } catch { + // ignore + } + }); + + afterEach(() => { + try { + if (fs.existsSync(toolStatePath)) fs.unlinkSync(toolStatePath); + } catch { + // ignore + } + }); + + it('defaults to enabled for unknown server/tool', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + expect(toolState.isToolEnabled('cluster-x', 'tool-a')).toBe(true); + }); + + it('setToolEnabled updates state and getDisabled/getEnabled reflect it', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + toolState.setToolEnabled('cluster-a', 'tool-1', false); + expect(toolState.isToolEnabled('cluster-a', 'tool-1')).toBe(false); + expect(toolState.getDisabledTools('cluster-a')).toContain('tool-1'); + expect(toolState.getEnabledTools('cluster-a')).not.toContain('tool-1'); + + toolState.setToolEnabled('cluster-a', 'tool-1', true); + expect(toolState.isToolEnabled('cluster-a', 'tool-1')).toBe(true); + expect(toolState.getEnabledTools('cluster-a')).toContain('tool-1'); + }); + + it('recordToolUsage increments usageCount and sets lastUsed (in-memory)', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + toolState.recordToolUsage('cluster-u', 'tool-u'); + toolState.recordToolUsage('cluster-u', 'tool-u'); + + const stats = toolState.getToolStats('cluster-u', 'tool-u'); + expect(stats).not.toBeNull(); + expect(stats?.usageCount).toBe(2); + expect(stats?.lastUsed).toBeInstanceOf(Date); + }); + + it('persists enabled state and usage to disk across instances', async () => { + const cfg1 = new MCPToolStateStore(toolStatePath); + await cfg1.initialize(); + cfg1.setToolEnabled('cluster-p', 'tool-p', false); + cfg1.recordToolUsage('cluster-p', 'tool-p'); + + // create a fresh instance which loads from the same file + const cfg2 = new MCPToolStateStore(toolStatePath); + await cfg2.initialize(); + expect(cfg2.isToolEnabled('cluster-p', 'tool-p')).toBe(false); + + const stats = cfg2.getToolStats('cluster-p', 'tool-p'); + // After load from JSON, lastUsed becomes a string; usageCount should persist as number + expect(stats?.usageCount).toBe(1); + expect( + typeof (stats as any)?.lastUsed === 'string' || (stats as any)?.lastUsed instanceof Date + ).toBe(true); + }); + + it('initializeToolsConfig creates and updates schemas and descriptions', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + const schemaA = { type: 'object', properties: { a: { type: 'string' } } }; + toolState.initializeToolsConfig('srv', [ + { name: 'tool-x', inputSchema: schemaA, description: 'desc x' }, + { name: 'tool-y', inputSchema: { type: 'string' }, description: 'desc y' }, + ]); + + const s1 = toolState.getToolStats('srv', 'tool-x'); + expect(s1).not.toBeNull(); + expect((s1 as any).inputSchema).toEqual(schemaA); + expect((s1 as any).description).toBe('desc x'); + + // re-initialize with changed schema/description for tool-x + const schemaA2 = { type: 'object', properties: { a: { type: 'number' } } }; + toolState.initializeToolsConfig('srv', [ + { name: 'tool-x', inputSchema: schemaA2, description: 'desc x updated' }, + ]); + + const s2 = toolState.getToolStats('srv', 'tool-x'); + expect((s2 as any).inputSchema).toEqual(schemaA2); + expect((s2 as any).description).toBe('desc x updated'); + }); + + it('replaceToolsConfig preserves enabled state and usageCount and removes missing tools', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + // create initial tools and modify state + toolState.initializeToolsConfig('cluster-r', [ + { name: 'keep-tool', inputSchema: null, description: 'keep' }, + { name: 'drop-tool', inputSchema: null, description: 'drop' }, + ]); + toolState.setToolEnabled('cluster-r', 'keep-tool', false); + // increment usage for keep-tool + toolState.recordToolUsage('cluster-r', 'keep-tool'); + toolState.recordToolUsage('cluster-r', 'keep-tool'); + + // replace with only keep-tool (and maybe new-tool) + toolState.replaceToolsConfig({ + 'cluster-r': [ + { name: 'keep-tool', inputSchema: null, description: 'keep' }, + { name: 'new-tool', inputSchema: null, description: 'new' }, + ], + }); + + // dropped tool should be gone + expect(toolState.getToolStats('cluster-r', 'drop-tool')).toBeNull(); + + // keep-tool should preserve enabled and usageCount + const keep = toolState.getToolStats('cluster-r', 'keep-tool'); + expect(keep).not.toBeNull(); + expect(keep?.usageCount).toBe(2); + expect(keep?.enabled).toBe(false); + + // new-tool should exist with defaults + const n = toolState.getToolStats('cluster-r', 'new-tool'); + expect(n).not.toBeNull(); + expect(n?.usageCount).toBe(0); + expect(n?.enabled).toBe(true); + }); + + it('getConfig, setConfig, replaceConfig and resetConfig behave as expected', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + toolState.resetConfig(); + expect(Object.keys(toolState.getConfig())).toHaveLength(0); + + const newConf = { + s1: { + t1: { enabled: false, usageCount: 3, inputSchema: null, description: 'd' }, + }, + }; + toolState.setConfig(newConf); + expect(toolState.getConfig()).toEqual(newConf); + + // replaceConfig should overwrite entirely + const replaced = { + s2: { + t2: { enabled: true, usageCount: 0, inputSchema: null, description: '' }, + }, + }; + toolState.replaceConfig(replaced); + expect(toolState.getConfig()).toEqual(replaced); + + // persisted to disk and load into new instance + const toolState2 = new MCPToolStateStore(toolStatePath); + await toolState2.initialize(); + expect(toolState2.getConfig()).toEqual(replaced); + }); +}); diff --git a/app/electron/mcp/MCPToolStateStore.ts b/app/electron/mcp/MCPToolStateStore.ts new file mode 100644 index 00000000000..7de01f57783 --- /dev/null +++ b/app/electron/mcp/MCPToolStateStore.ts @@ -0,0 +1,394 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; + +/** + * State of a single MCP tool. + */ +export interface MCPToolState { + /** + * Whether the tool is enabled or disabled + */ + enabled: boolean; + /** + * Timestamp of the last time the tool was used + */ + lastUsed?: Date; + /** + * Number of times the tool has been used + */ + usageCount?: number; + /** + * JSON schema for tool parameters + */ + inputSchema?: any; + /** + * Description of the tool from MCP server + */ + description?: string; +} + +/** + * State of all MCP tools for a specific server. + */ +export interface MCPServerToolState { + [toolName: string]: MCPToolState; +} + +/** + * Configuration for MCP tools across multiple servers. + */ +export interface MCPToolsConfig { + [serverName: string]: MCPServerToolState; +} + +/** + * MCPToolStateStore manages configuration for MCP (Multi-Cluster Platform) + * tools, including enabled/disabled state and usage statistics. + * + * Example: + * ```ts + * const toolStatePath = path.join(app.getPath('userData'), 'mcp-tools-config.json'); + * const toolState = new MCPToolStateStore(toolStatePath); + * + * // Example inputSchema + * const exampleSchema = { + * type: 'object', + * properties: { + * param1: { type: 'string', description: 'Parameter 1' }, + * param2: { type: 'number', description: 'Parameter 2' }, + * }, + * required: ['param1'], + * }; + * + * // Initialize default config for available tools + * toolState.initializeToolsConfig('my-cluster', [ + * { name: 'tool-a', inputSchema: exampleSchema, description: 'Tool A description' }, + * { name: 'tool-b', inputSchema: exampleSchema, description: 'Tool B description' }, + * ]); + * + * // Enable or disable a tool + * toolState.setToolEnabled('my-cluster', 'tool-a', false); + * + * // Check if a tool is enabled + * const isEnabled = toolState.isToolEnabled('my-cluster', 'tool-a'); + * + * // Get all disabled tools for a server + * const disabledTools = toolState.getDisabledTools('my-cluster'); + * + * // Record tool usage + * toolState.recordToolUsage('my-cluster', 'tool-a'); + * + * // Get tool statistics + * const toolStats = toolState.getToolStats('my-cluster', 'tool-a'); + * + * // Replace entire tools configuration + * toolState.replaceToolsConfig({ + * 'my-cluster': [ + * { name: 'tool-a', inputSchema: {...}, description: 'Tool A description' }, + * { name: 'tool-b', inputSchema: {...}, description: 'Tool B description' }, + * ], + * 'another-cluster': [ + * { name: 'tool-c', inputSchema: {...}, description: 'Tool C description' }, + * ], + * }); + * + * // Reset configuration + * toolState.resetConfig(); + * + * // Get the complete configuration + * const completeConfig = toolState.getConfig(); + * + * // Set the complete configuration + * toolState.setConfig(completeConfig); + * ``` + */ +export class MCPToolStateStore { + private toolStatePath: string; + private config: MCPToolsConfig = {}; + + constructor(configPath: string) { + this.toolStatePath = configPath; + } + + /** + * Initialize the MCP client. + */ + async initialize(): Promise { + return await this.loadConfig(); + } + + /** + * Load MCP tools configuration from file + */ + private async loadConfig(): Promise { + try { + await fs.promises.access(this.toolStatePath, fs.constants.F_OK); + const configData = await fs.promises.readFile(this.toolStatePath, 'utf-8'); + this.config = JSON.parse(configData); + } catch (error) { + // If file doesn't exist or any error occurs, fall back to empty config + if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') { + this.config = {}; + } else { + console.error('Error loading MCP tools configuration:', error); + this.config = {}; + } + } + } + + /** + * Save MCP tools configuration to file + */ + private saveConfig(): void { + try { + fs.writeFileSync(this.toolStatePath, JSON.stringify(this.config, null, 2), 'utf-8'); + } catch (error) { + console.error('Error saving MCP tools configuration:', error); + } + } + + /** + * Get the enabled state of a specific tool + */ + isToolEnabled(serverName: string, toolName: string): boolean { + const serverConfig = this.config[serverName]; + if (!serverConfig) { + // Default to enabled for new tools + return true; + } + + const toolState = serverConfig[toolName]; + if (!toolState) { + // Default to enabled for new tools + return true; + } + + return toolState.enabled; + } + + /** + * Set the enabled state of a specific tool + */ + setToolEnabled(serverName: string, toolName: string, enabled: boolean): void { + if (!this.config[serverName]) { + this.config[serverName] = {}; + } + + if (!this.config[serverName][toolName]) { + this.config[serverName][toolName] = { + enabled: true, + usageCount: 0, + }; + } + + this.config[serverName][toolName].enabled = enabled; + this.saveConfig(); + } + + /** + * Get all disabled tools for a server + */ + getDisabledTools(serverName: string): string[] { + const serverConfig = this.config[serverName]; + if (!serverConfig) { + return []; + } + + return Object.entries(serverConfig) + .filter(([, toolState]) => !toolState.enabled) + .map(([toolName]) => toolName); + } + + /** + * Get all enabled tools for a server + */ + getEnabledTools(serverName: string): string[] { + const serverConfig = this.config[serverName]; + if (!serverConfig) { + return []; + } + + return Object.entries(serverConfig) + .filter(([, toolState]) => toolState.enabled) + .map(([toolName]) => toolName); + } + + /** + * Update tool usage statistics + */ + recordToolUsage(serverName: string, toolName: string): void { + if (!this.config[serverName]) { + this.config[serverName] = {}; + } + + if (!this.config[serverName][toolName]) { + this.config[serverName][toolName] = { + enabled: true, + usageCount: 0, + }; + } + + const toolState = this.config[serverName][toolName]; + toolState.lastUsed = new Date(); + toolState.usageCount = (toolState.usageCount || 0) + 1; + this.saveConfig(); + } + + /** + * Get the complete configuration + */ + getConfig(): MCPToolsConfig { + return { ...this.config }; + } + + /** + * Set the complete configuration + */ + setConfig(newConfig: MCPToolsConfig): void { + this.config = { ...newConfig }; + this.saveConfig(); + } + + /** + * Reset configuration to empty state + */ + resetConfig(): void { + this.config = {}; + this.saveConfig(); + } + + /** + * Initialize default configuration for available tools with schemas + */ + initializeToolsConfig( + serverName: string, + toolsInfo: Array<{ + name: string; + inputSchema?: any; + description?: string; + }> + ): void { + if (!this.config[serverName]) { + this.config[serverName] = {}; + } + + const serverConfig = this.config[serverName]; + let hasChanges = false; + + for (const toolInfo of toolsInfo) { + const toolName = toolInfo.name; + + if (!serverConfig[toolName]) { + serverConfig[toolName] = { + enabled: true, + usageCount: 0, + inputSchema: toolInfo.inputSchema || null, + description: toolInfo.description || '', + }; + hasChanges = true; + } else { + // Always update schema and description for existing tools + let toolChanged = false; + + // Update schema if it's different or missing + const currentSchema = JSON.stringify(serverConfig[toolName].inputSchema || null); + const newSchema = JSON.stringify(toolInfo.inputSchema || null); + if (currentSchema !== newSchema) { + serverConfig[toolName].inputSchema = toolInfo.inputSchema || null; + toolChanged = true; + } + + // Update description if it's different or missing + const currentDescription = serverConfig[toolName].description || ''; + const newDescription = toolInfo.description || ''; + if (currentDescription !== newDescription) { + serverConfig[toolName].description = newDescription; + toolChanged = true; + } + + if (toolChanged) { + hasChanges = true; + } + } + } + + if (hasChanges) { + this.saveConfig(); + } + } + + /** + * Get tool statistics + */ + getToolStats(serverName: string, toolName: string): MCPToolState | null { + const serverConfig = this.config[serverName]; + if (!serverConfig || !serverConfig[toolName]) { + return null; + } + + return { ...serverConfig[toolName] }; + } + + /** + * Replace the entire tools configuration with a new set of tools + * This overwrites all existing tools with only the current ones + */ + replaceToolsConfig( + toolsByServer: Record< + string, + Array<{ + name: string; + inputSchema?: any; + description?: string; + }> + > + ): void { + // Create a new config object + const newConfig: MCPToolsConfig = {}; + + for (const [serverName, toolsInfo] of Object.entries(toolsByServer)) { + newConfig[serverName] = {}; + + for (const toolInfo of toolsInfo) { + const toolName = toolInfo.name; + + // Check if this tool existed in the old config to preserve enabled state and usage count + const oldToolState = this.config[serverName]?.[toolName]; + + newConfig[serverName][toolName] = { + enabled: oldToolState?.enabled ?? true, // Preserve enabled state or default to true + usageCount: oldToolState?.usageCount ?? 0, // Preserve usage count or default to 0 + inputSchema: toolInfo.inputSchema || null, + description: toolInfo.description || '', + }; + } + } + + // Replace the entire config + this.config = newConfig; + this.saveConfig(); + } + + /** + * Replace the entire configuration with a new config object + */ + replaceConfig(newConfig: MCPToolsConfig): void { + this.config = newConfig; + this.saveConfig(); + } +} diff --git a/app/package.json b/app/package.json index a3cdca7d649..e4ad5aa5753 100644 --- a/app/package.json +++ b/app/package.json @@ -124,7 +124,8 @@ "electron/env-paths.js", "electron/plugin-management.js", "electron/runCmd.js", - "electron/mcp/MCPClient.js" + "electron/mcp/MCPClient.js", + "electron/mcp/MCPToolStateStore.js" ], "extraResources": [ { From d99a95dc6deba507fbfc566ca1a8e920bdec369c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Tue, 4 Nov 2025 16:11:26 -0300 Subject: [PATCH 04/20] app: MCPToolStateStore: Add parseServerNameToolName to get separate components Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPClient.test.ts | 19 +++++++++++++ app/electron/mcp/MCPToolStateStore.test.ts | 23 +++++++++++++++- app/electron/mcp/MCPToolStateStore.ts | 32 ++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/app/electron/mcp/MCPClient.test.ts b/app/electron/mcp/MCPClient.test.ts index 6258979649b..3745c665de0 100644 --- a/app/electron/mcp/MCPClient.test.ts +++ b/app/electron/mcp/MCPClient.test.ts @@ -111,3 +111,22 @@ describe('MCPClient', () => { expect(infoSpy).not.toHaveBeenCalledWith('MCPClient: cleaned up'); }); }); + +describe('MCPClient logging behavior', () => { + it('logs clusters change even when not initialized', async () => { + const cfgPath = path.join(os.tmpdir(), `mcp-test-${Date.now()}-${Math.random()}.json`); + const client = new (require('./MCPClient').default)(cfgPath) as InstanceType< + typeof import('./MCPClient').default + >; + + const infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}) as jest.Mock; + + await expect(client.handleClustersChange(['cluster-log'])).rejects.toThrow( + 'MCPClient: not initialized' + ); + + expect(infoSpy).toHaveBeenCalledWith('MCPClient: clusters changed ->', ['cluster-log']); + + infoSpy.mockRestore(); + }); +}); diff --git a/app/electron/mcp/MCPToolStateStore.test.ts b/app/electron/mcp/MCPToolStateStore.test.ts index 23268df5594..9b497b35d2d 100644 --- a/app/electron/mcp/MCPToolStateStore.test.ts +++ b/app/electron/mcp/MCPToolStateStore.test.ts @@ -17,7 +17,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { MCPToolStateStore } from './MCPToolStateStore'; +import { MCPToolStateStore, parseServerNameToolName } from './MCPToolStateStore'; function tmpPath(): string { return path.join(os.tmpdir(), `mcp-test-${Date.now()}-${Math.random()}.json`); @@ -189,3 +189,24 @@ describe('MCPConfig', () => { expect(toolState2.getConfig()).toEqual(replaced); }); }); + +describe('parseServerNameToolName', () => { + it('returns default serverName when no separator is present', () => { + const res = parseServerNameToolName('kubectl'); + expect(res.serverName).toBe('default'); + expect(res.toolName).toBe('kubectl'); + }); + + it('splits server and tool when a single separator is present', () => { + const res = parseServerNameToolName('myserver__helm'); + + expect(res.serverName).toBe('myserver'); + expect(res.toolName).toBe('helm'); + }); + + it('preserves additional separators in the toolName when multiple separators are present', () => { + const res = parseServerNameToolName('myserver__helm__test'); + expect(res.serverName).toBe('myserver'); + expect(res.toolName).toBe('helm__test'); + }); +}); diff --git a/app/electron/mcp/MCPToolStateStore.ts b/app/electron/mcp/MCPToolStateStore.ts index 7de01f57783..dbfbab89d6b 100644 --- a/app/electron/mcp/MCPToolStateStore.ts +++ b/app/electron/mcp/MCPToolStateStore.ts @@ -392,3 +392,35 @@ export class MCPToolStateStore { this.saveConfig(); } } + +/** + * Parse tool name to extract server name and tool name components. + * + * @param fullToolName - The full tool name string, potentially including server name. + * + * @returns An object containing serverName and toolName. + * + * @example + * ```ts + * parseServerNameToolName('myserver__helm') + * // returns { serverName: 'myserver', toolName: 'helm' } + * parseServerNameToolName('kubectl') + * // returns { serverName: 'default', toolName: 'kubectl' } + * ``` + */ +export function parseServerNameToolName(fullToolName: string): { + serverName: string; + toolName: string; +} { + const parts = fullToolName.split('__'); + if (parts.length >= 2) { + return { + serverName: parts[0], + toolName: parts.slice(1).join('__'), + }; + } + return { + serverName: 'default', + toolName: fullToolName, + }; +} From 814f6e4128a61a83a7cfd29ab989d22f31ed4f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Tue, 4 Nov 2025 16:47:56 -0300 Subject: [PATCH 05/20] app: MCPToolStateStore: Add summarizeMcpToolStateChanges and tests This summarizes MCP tool state changes (added, removed, enabled, disabled) across servers and include unit tests covering key cases. Useful for showing concise change counts and ENABLE/DISABLE summaries in the UI before applying config updates. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPToolStateStore.test.ts | 116 ++++++++++++++++++++- app/electron/mcp/MCPToolStateStore.ts | 82 +++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) diff --git a/app/electron/mcp/MCPToolStateStore.test.ts b/app/electron/mcp/MCPToolStateStore.test.ts index 9b497b35d2d..13ff51c79e4 100644 --- a/app/electron/mcp/MCPToolStateStore.test.ts +++ b/app/electron/mcp/MCPToolStateStore.test.ts @@ -17,7 +17,11 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { MCPToolStateStore, parseServerNameToolName } from './MCPToolStateStore'; +import { + MCPToolStateStore, + parseServerNameToolName, + summarizeMcpToolStateChanges, +} from './MCPToolStateStore'; function tmpPath(): string { return path.join(os.tmpdir(), `mcp-test-${Date.now()}-${Math.random()}.json`); @@ -210,3 +214,113 @@ describe('parseServerNameToolName', () => { expect(res.toolName).toBe('helm__test'); }); }); + +describe('summarizeMcpToolStateChanges', () => { + it('returns zero changes for identical empty configs', () => { + const res = summarizeMcpToolStateChanges({}, {}); + expect(res.totalChanges).toBe(0); + expect(res.summaryText).toBe(''); + }); + + it('counts added enabled tools and includes them in ENABLE summary', () => { + const current = {}; + const nw = { + srv1: { + 'tool-a': { enabled: true }, + }, + }; + const res = summarizeMcpToolStateChanges(current, nw); + // addedTools (1) + enabledTools (1) => total 2 + expect(res.totalChanges).toBe(2); + expect(res.summaryText).toContain('✓ ENABLE (1)'); + expect(res.summaryText).toContain('tool-a (srv1)'); + expect(res.summaryText).not.toContain('✗ DISABLE'); + }); + + it('counts removed tools even when no summary lines are produced', () => { + const current = { + srv1: { + 'tool-x': { enabled: true }, + }, + }; + const nw = {}; + const res = summarizeMcpToolStateChanges(current, nw); + // removedTools (1) => total 1 + expect(res.totalChanges).toBe(1); + // removed tools are not printed in summaryText, so should be empty + expect(res.summaryText).toBe(''); + }); + + it('detects enable/disable changes between configs', () => { + const current = { + srvA: { + 'tool-1': { enabled: true }, + 'tool-2': { enabled: false }, + }, + }; + const nw = { + srvA: { + 'tool-1': { enabled: false }, // changed to disabled + 'tool-2': { enabled: true }, // changed to enabled + }, + }; + const res = summarizeMcpToolStateChanges(current, nw); + // two changes (one disabled, one enabled) + expect(res.totalChanges).toBe(2); + expect(res.summaryText).toContain('✓ ENABLE (1): tool-2 (srvA)'); + expect(res.summaryText).toContain('✗ DISABLE (1): tool-1 (srvA)'); + // Ensure ENABLE and DISABLE sections are separated by a blank line + expect(res.summaryText.split('\n\n').length).toBeGreaterThanOrEqual(2); + }); + + it('aggregates changes across multiple servers', () => { + // This test verifies that summarizeMcpToolConfigChanges correctly + // aggregates changes across multiple servers, counting: + // - added tools (whether enabled or disabled), + // - removed tools (which are counted but not included in the human-readable summary), + // - tools that changed enabled state (enable -> disable and vice-versa). + // It ensures both the numeric totals and the generated ENABLE/DISABLE summary + // lines include the expected entries and server names. + const current = { + s1: { a: { enabled: true } }, + s2: { x: { enabled: false } }, + }; + const nw = { + s1: { a: { enabled: false }, b: { enabled: true } }, // a->disabled, b added+enabled + s2: { + /* x removed */ + }, + s3: { y: { enabled: false } }, // new disabled + }; + const res = summarizeMcpToolStateChanges(current, nw); + // Changes: a (changed), b (added), x (removed), y (added) + // enabledTools: b (added enabled) => 1 + // disabledTools: a (changed), y (added disabled) => 2 + // addedTools: b,y => 2 + // removedTools: x => 1 + // total = 1+2+2+1 = 6 + expect(res.totalChanges).toBe(6); + expect(res.summaryText).toContain('✓ ENABLE (1)'); + expect(res.summaryText).toContain('✗ DISABLE (2)'); + expect(res.summaryText).toContain('b (s1)'); + expect(res.summaryText).toContain('a (s1)'); + expect(res.summaryText).toContain('y (s3)'); + }); + + it('ignores non-enabled metadata-only changes (description/inputSchema)', () => { + const current = { + srvM: { + 'tool-meta': { enabled: true, description: 'old', inputSchema: { type: 'string' } }, + }, + }; + const nw = { + srvM: { + 'tool-meta': { enabled: true, description: 'new', inputSchema: { type: 'string' } }, + }, + }; + const res = summarizeMcpToolStateChanges(current, nw); + // Only metadata changed; enabled state unchanged => no counted changes + expect(res.totalChanges).toBe(0); + expect(res.summaryText).toBe(''); + }); +}); diff --git a/app/electron/mcp/MCPToolStateStore.ts b/app/electron/mcp/MCPToolStateStore.ts index dbfbab89d6b..03858dd110d 100644 --- a/app/electron/mcp/MCPToolStateStore.ts +++ b/app/electron/mcp/MCPToolStateStore.ts @@ -56,6 +56,88 @@ export interface MCPToolsConfig { [serverName: string]: MCPServerToolState; } +/** + * Create a summary of changes between two MCP tool configurations. + * + * @param currentConfig - The current MCP tools configuration. + * @param newConfig - The new MCP tools configuration. + * + * @returns An object containing the total number of changes and a summary text. + */ +export function summarizeMcpToolStateChanges( + currentConfig: Record>, + newConfig: Record> +): { totalChanges: number; summaryText: string } { + const enabledTools: string[] = []; + const disabledTools: string[] = []; + const addedTools: string[] = []; + const removedTools: string[] = []; + + // Get all server names from both configs + const allServers = new Set([ + ...Object.keys(currentConfig || {}), + ...Object.keys(newConfig || {}), + ]); + + for (const serverName of allServers) { + const currentServerConfig = currentConfig[serverName] || {}; + const newServerConfig = newConfig[serverName] || {}; + + // Get all tool names from both configs + const allTools = new Set([ + ...Object.keys(currentServerConfig), + ...Object.keys(newServerConfig), + ]); + + for (const toolName of allTools) { + const currentTool = currentServerConfig[toolName]; + const newTool = newServerConfig[toolName]; + const displayName = `${toolName} (${serverName})`; + + if (!currentTool && newTool) { + // New tool added + addedTools.push(displayName); + if (newTool.enabled) { + enabledTools.push(displayName); + } else { + disabledTools.push(displayName); + } + } else if (currentTool && !newTool) { + // Tool removed + removedTools.push(displayName); + } else if (currentTool && newTool) { + // Tool modified + if (currentTool.enabled !== newTool.enabled) { + if (newTool.enabled) { + enabledTools.push(displayName); + } else { + disabledTools.push(displayName); + } + } + } + } + } + + // Build summary text + const summaryParts: string[] = []; + + if (enabledTools.length > 0) { + summaryParts.push(`✓ ENABLE (${enabledTools.length}): ${enabledTools.join(', ')}`); + } + + if (disabledTools.length > 0) { + summaryParts.push(`✗ DISABLE (${disabledTools.length}): ${disabledTools.join(', ')}`); + } + + const totalChanges = + enabledTools.length + disabledTools.length + addedTools.length + removedTools.length; + + return { + totalChanges, + summaryText: summaryParts.join('\n\n'), + }; +} + /** * MCPToolStateStore manages configuration for MCP (Multi-Cluster Platform) * tools, including enabled/disabled state and usage statistics. From f911b7494f9a069f6f10ca7453d6bde02cb3f246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 6 Nov 2025 07:24:32 -0300 Subject: [PATCH 06/20] app: MCPToolStateStore: Add initConfigFromClientTools This initialzes the MCPToolStateStore with tools from MultiServerMCPClient.getTools(). Completely replacing the existing config with these current tools. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPToolStateStore.test.ts | 103 +++++++++++++++++++++ app/electron/mcp/MCPToolStateStore.ts | 54 +++++++++++ 2 files changed, 157 insertions(+) diff --git a/app/electron/mcp/MCPToolStateStore.test.ts b/app/electron/mcp/MCPToolStateStore.test.ts index 13ff51c79e4..53c4b475fbe 100644 --- a/app/electron/mcp/MCPToolStateStore.test.ts +++ b/app/electron/mcp/MCPToolStateStore.test.ts @@ -324,3 +324,106 @@ describe('summarizeMcpToolStateChanges', () => { expect(res.summaryText).toBe(''); }); }); + +describe('initConfigFromClientTools', () => { + let toolStatePath: string; + + beforeEach(() => { + toolStatePath = tmpPath(); + try { + if (fs.existsSync(toolStatePath)) fs.unlinkSync(toolStatePath); + } catch { + // ignore + } + }); + + afterEach(() => { + try { + if (fs.existsSync(toolStatePath)) fs.unlinkSync(toolStatePath); + } catch { + // ignore + } + }); + + it('clears config when no client tools are provided', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + // Seed with some config + toolState.setConfig({ + someServer: { + someTool: { enabled: false, usageCount: 2, inputSchema: null, description: '' }, + }, + }); + expect(Object.keys(toolState.getConfig()).length).toBeGreaterThan(0); + + // init with empty client tools should clear the config + toolState.initConfigFromClientTools([]); + expect(Object.keys(toolState.getConfig())).toHaveLength(0); + }); + + it('groups tools by server, extracts schema and description, and sets defaults', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + const clientTools: any[] = [ + { + name: 'srvA__tool-x', + schema: { type: 'object', properties: { a: { type: 'string' } } }, + description: 'desc x', + }, + { name: 'tool-no-server', description: 'global tool' }, // no schema provided + ]; + + toolState.initConfigFromClientTools(clientTools); + + const sx = toolState.getToolStats('srvA', 'tool-x'); + expect(sx).not.toBeNull(); + expect((sx as any).inputSchema).toEqual({ + type: 'object', + properties: { a: { type: 'string' } }, + }); + expect((sx as any).description).toBe('desc x'); + expect(sx?.enabled).toBe(true); + expect(sx?.usageCount).toBe(0); + + const gn = toolState.getToolStats('default', 'tool-no-server'); + expect(gn).not.toBeNull(); + expect((gn as any).inputSchema).toBeNull(); + expect((gn as any).description).toBe('global tool'); + expect(gn?.enabled).toBe(true); + }); + + it('preserves enabled state and usageCount from existing config when tool still exists', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + // Seed with a tool that has specific enabled/usage values + toolState.setConfig({ + myServer: { + preservedTool: { + enabled: false, + usageCount: 5, + inputSchema: null, + description: 'old desc', + }, + }, + }); + + // Client reports same tool (with updated schema/description) + const clientTools: any[] = [ + { name: 'myServer__preservedTool', schema: { type: 'string' }, description: 'new desc' }, + ]; + + toolState.initConfigFromClientTools(clientTools); + + const p = toolState.getToolStats('myServer', 'preservedTool'); + expect(p).not.toBeNull(); + // preserved values should remain + expect(p?.enabled).toBe(false); + expect(p?.usageCount).toBe(5); + // schema/description should be updated from client tools + expect((p as any).inputSchema).toEqual({ type: 'string' }); + expect((p as any).description).toBe('new desc'); + }); +}); diff --git a/app/electron/mcp/MCPToolStateStore.ts b/app/electron/mcp/MCPToolStateStore.ts index 03858dd110d..4e820c15f39 100644 --- a/app/electron/mcp/MCPToolStateStore.ts +++ b/app/electron/mcp/MCPToolStateStore.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import type { DynamicStructuredTool } from '@langchain/core/dist/tools/index'; import * as fs from 'fs'; /** @@ -473,6 +474,59 @@ export class MCPToolStateStore { this.config = newConfig; this.saveConfig(); } + + /** + * Initialize tools configuration for all available tools from client tools. + * This completely replaces the existing config with current tools. + * + * @param clientTools - Array of available tools with their schemas. From MultiServerMCPClient.getTools() + */ + initConfigFromClientTools(clientTools: DynamicStructuredTool[]): void { + if (!clientTools || clientTools.length === 0) { + console.log('No tools available for configuration initialization'); + // Clear the config if no tools are available + this.replaceConfig({}); + return; + } + + // Group tools by server name with their schemas + const toolsByServer: Record< + string, + Array<{ + name: string; + inputSchema?: any; + description?: string; + }> + > = {}; + + for (const tool of clientTools) { + // Extract server name from tool name (format: "serverName__toolName") + const { serverName, toolName } = parseServerNameToolName(tool.name); + + // Extract schema from the tool (LangChain tools use .schema property) + const toolSchema = tool.schema || (tool as any).inputSchema || null; + console.log( + `Processing tool: ${toolName}, has inputSchema: ${!!toolSchema}, description: "${ + tool.description + }"` + ); + + if (!toolsByServer[serverName]) { + toolsByServer[serverName] = []; + } + + toolsByServer[serverName].push({ + name: toolName, + inputSchema: toolSchema, + description: tool.description || '', + }); + } + + console.log('Tools grouped by server:', Object.keys(toolsByServer)); + + // Replace the entire configuration with current tools + this.replaceToolsConfig(toolsByServer); + } } /** From 6d1589c2c86160c5fe2a51d2067ae955262c55ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 6 Nov 2025 10:36:28 -0300 Subject: [PATCH 07/20] app: MCPToolStateStore: Add validateToolArgs For validating tool arguments against a schema. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPToolStateStore.test.ts | 77 ++++++++++++++ app/electron/mcp/MCPToolStateStore.ts | 114 +++++++++++++++++++++ 2 files changed, 191 insertions(+) diff --git a/app/electron/mcp/MCPToolStateStore.test.ts b/app/electron/mcp/MCPToolStateStore.test.ts index 53c4b475fbe..65a59043dc4 100644 --- a/app/electron/mcp/MCPToolStateStore.test.ts +++ b/app/electron/mcp/MCPToolStateStore.test.ts @@ -21,6 +21,7 @@ import { MCPToolStateStore, parseServerNameToolName, summarizeMcpToolStateChanges, + validateToolArgs, } from './MCPToolStateStore'; function tmpPath(): string { @@ -427,3 +428,79 @@ describe('initConfigFromClientTools', () => { expect((p as any).description).toBe('new desc'); }); }); + +describe('validateToolArgs', () => { + it('returns valid when schema is null', () => { + const res = validateToolArgs(null, { any: 1 }); + expect(res.valid).toBe(true); + expect(res.error).toBeUndefined(); + }); + + it('fails when a required property is missing', () => { + const schema = { + type: 'object', + required: ['name'], + properties: { + name: { type: 'string' }, + }, + }; + const res = validateToolArgs(schema, {}); + expect(res.valid).toBe(false); + expect(res.error).toContain("Required parameter 'name'"); + }); + + it('fails when property type does not match (number expected)', () => { + const schema = { + type: 'object', + properties: { + age: { type: 'number' }, + }, + }; + const res = validateToolArgs(schema, { age: 'not-a-number' }); + expect(res.valid).toBe(false); + expect(res.error).toContain('should be a number'); + }); + + it('validates array types correctly', () => { + const schema = { + type: 'object', + properties: { + items: { type: 'array' }, + }, + }; + const ok = validateToolArgs(schema, { items: [1, 2, 3] }); + expect(ok.valid).toBe(true); + + const notOk = validateToolArgs(schema, { items: 'not-an-array' }); + expect(notOk.valid).toBe(false); + expect(notOk.error).toContain('should be an array'); + }); + + it('validates object types and rejects arrays/null for object type', () => { + const schema = { + type: 'object', + properties: { + cfg: { type: 'object' }, + }, + }; + expect(validateToolArgs(schema, { cfg: { a: 1 } }).valid).toBe(true); + const asArray = validateToolArgs(schema, { cfg: [1, 2] }); + expect(asArray.valid).toBe(false); + expect(asArray.error).toContain('should be an object'); + const asNull = validateToolArgs(schema, { cfg: null }); + expect(asNull.valid).toBe(false); + expect(asNull.error).toContain('should be an object'); + }); + + it('treats unsupported property types as non-fatal and returns valid', () => { + const schema = { + type: 'object', + properties: { + count: { type: 'integer' }, // unsupported type in validator + }, + }; + const res = validateToolArgs(schema, { count: 42 }); + expect(res.valid).toBe(true); + expect(res.error).toBeUndefined(); + }); +}); diff --git a/app/electron/mcp/MCPToolStateStore.ts b/app/electron/mcp/MCPToolStateStore.ts index 4e820c15f39..ccbbd17ab9c 100644 --- a/app/electron/mcp/MCPToolStateStore.ts +++ b/app/electron/mcp/MCPToolStateStore.ts @@ -560,3 +560,117 @@ export function parseServerNameToolName(fullToolName: string): { toolName: fullToolName, }; } + +/** + * Validate tool arguments against tool schema. + * + * Note: this validates as true if it doesn't recognize the schema format, + * and validates as true if it doesn't cover the type of the input. + * + * @todo: @langchain/mcp-adapters does validation internally, so this may be redundant? + * + * @param schema - The tool's input schema. + * From toolState.inputSchema from @langchain/mcp-adapters tool input schemas. + * @param args - The arguments to validate. + * + * @returns An object indicating whether the arguments are valid and any error message. + * + * @example + * ```ts + * const schema = { + * type: 'object', + * properties: { + * param1: { type: 'string' }, + * param2: { type: 'number' }, + * }, + * required: ['param1'], + * }; + * const args = { param1: 'value1', param2: 42 }; + * const result = validateToolArgs(schema, args); + * // result: { valid: true } + * ``` + */ +export function validateToolArgs( + schema: MCPToolState['inputSchema'] | null, + args: Record +): { valid: boolean; error?: string } { + if (!schema) { + // No schema available, assume valid + return { valid: true }; + } + + try { + // Basic validation - check required properties + if (schema.required && Array.isArray(schema.required)) { + for (const requiredProp of schema.required) { + if (args[requiredProp] === undefined || args[requiredProp] === null) { + return { + valid: false, + error: `Required parameter '${requiredProp}' is missing`, + }; + } + } + } + + // Check property types if schema properties are defined + if (schema.properties) { + for (const [propName, propSchema] of Object.entries(schema.properties as any)) { + if (args[propName] !== undefined) { + const propType = (propSchema as any).type; + const actualType = typeof args[propName]; + + if (propType === 'string' && actualType !== 'string') { + return { + valid: false, + error: `Parameter '${propName}' should be a string, got ${actualType}`, + }; + } + if (propType === 'number' && actualType !== 'number') { + return { + valid: false, + error: `Parameter '${propName}' should be a number, got ${actualType}`, + }; + } + if (propType === 'boolean' && actualType !== 'boolean') { + return { + valid: false, + error: `Parameter '${propName}' should be a boolean, got ${actualType}`, + }; + } + if (propType === 'array' && !Array.isArray(args[propName])) { + return { + valid: false, + error: `Parameter '${propName}' should be an array, got ${actualType}`, + }; + } + if ( + propType === 'object' && + (actualType !== 'object' || Array.isArray(args[propName]) || args[propName] === null) + ) { + return { + valid: false, + error: `Parameter '${propName}' should be an object, got ${actualType}`, + }; + } + + // If the types are not covered above? We warn, and skip validation. + if (!['string', 'number', 'boolean', 'array', 'object'].includes(propType)) { + console.warn(`Unsupported parameter type in schema: ${propType}`); + + // return { + // valid: false, + // error: `Unsupported parameter type '${propType}' for '${propName}'`, + // }; + } + } + } + } + + return { valid: true }; + } catch (error) { + return { + valid: false, + error: `Schema validation error: ${error instanceof Error ? error.message : 'Unknown error'}`, + }; + } +} From 2802aed645eab95c5ddf0c2a48bbd280786ebea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 6 Nov 2025 12:30:56 -0300 Subject: [PATCH 08/20] app: MCPToolStateStore: Add showToolsConfigConfirmationDialog Show detailed confirmation dialog for tools configuration changes. Compares current and new configurations and displays a summary of changes. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPToolStateStore.ts | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/electron/mcp/MCPToolStateStore.ts b/app/electron/mcp/MCPToolStateStore.ts index ccbbd17ab9c..148b5d40ed8 100644 --- a/app/electron/mcp/MCPToolStateStore.ts +++ b/app/electron/mcp/MCPToolStateStore.ts @@ -15,6 +15,7 @@ */ import type { DynamicStructuredTool } from '@langchain/core/dist/tools/index'; +import { type BrowserWindow, dialog } from 'electron'; import * as fs from 'fs'; /** @@ -674,3 +675,33 @@ export function validateToolArgs( }; } } + +/** + * Show detailed confirmation dialog for tools configuration changes. + * Compares current and new configurations and displays a summary of changes. + * + * @param mainWindow - The main BrowserWindow to parent the dialog. + * @param currentConfig - The current configuration. + * @param nextConfig - The new configuration to be applied. + * + * @returns Promise resolving to true if user approves changes, false otherwise + */ +export async function showToolsConfigConfirmationDialog( + mainWindow: BrowserWindow, + currentConfig: MCPToolsConfig, + nextConfig: MCPToolsConfig +): Promise { + const summary = summarizeMcpToolStateChanges(currentConfig, nextConfig); + if (summary.totalChanges === 0) { + return true; // No changes, allow operation + } + const result = await dialog.showMessageBox(mainWindow, { + type: 'question', + buttons: ['Apply Changes', 'Cancel'], + defaultId: 1, + title: 'MCP Tools Configuration Changes', + message: `${summary.totalChanges} tool configuration change(s) will be applied:`, + detail: summary.summaryText + '\n\nDo you want to apply these changes?', + }); + return result.response === 0; // 0 is "Apply Changes" +} From d6552e9bd40f972bbba4846618f85c47ba769d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Wed, 5 Nov 2025 19:02:18 -0300 Subject: [PATCH 09/20] app: settings: runCmd: Extract loadSettings saveSettings into own module Co-Authored-By: Ashu Ghildiyal --- app/.gitignore | 2 + app/electron/runCmd.ts | 40 ++++-------------- app/electron/settings.test.ts | 76 +++++++++++++++++++++++++++++++++++ app/electron/settings.ts | 43 ++++++++++++++++++++ app/package.json | 3 +- 5 files changed, 131 insertions(+), 33 deletions(-) create mode 100644 app/electron/settings.test.ts create mode 100644 app/electron/settings.ts diff --git a/app/.gitignore b/app/.gitignore index 312da5dbfbc..68a2301d54a 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -17,3 +17,5 @@ electron/mcp/MCPClient.js electron/mcp/MCPClient.test.js electron/mcp/MCPToolStateStore.js electron/mcp/MCPToolStateStore.test.js +electron/settings.js +electron/settings.test.js diff --git a/app/electron/runCmd.ts b/app/electron/runCmd.ts index 88c498dd82e..20d60d95c06 100644 --- a/app/electron/runCmd.ts +++ b/app/electron/runCmd.ts @@ -15,13 +15,13 @@ */ import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; -import { app, BrowserWindow, dialog } from 'electron'; +import { BrowserWindow, dialog } from 'electron'; import { IpcMainEvent } from 'electron/main'; import crypto from 'node:crypto'; -import fs from 'node:fs'; import path from 'path'; import i18n from './i18next.config'; import { defaultPluginsDir } from './plugin-management'; +import { loadSettings, saveSettings, SETTINGS_PATH } from './settings'; /** * Data sent from the renderer process when a 'run-command' event is emitted. @@ -65,30 +65,6 @@ function confirmCommandDialog(command: string, mainWindow: BrowserWindow): boole return resp === 0; } -const SETTINGS_PATH = path.join(app?.getPath('userData') || 'testing', 'settings.json'); - -/** - * Loads the user settings. - * If the settings file does not exist, an empty object is returned. - * @returns The settings object. - */ -function loadSettings(): Record { - try { - const data = fs.readFileSync(SETTINGS_PATH, 'utf-8'); - return JSON.parse(data); - } catch (error) { - return {}; - } -} - -/** - * Saves the user settings. - * @param settings - The settings object to save. - */ -function saveSettings(settings: Record) { - fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings), 'utf-8'); -} - /** * Checks if the user has already consented to running the command. * @@ -99,7 +75,7 @@ function saveSettings(settings: Record) { * @returns true if the user has consented to running the command, false otherwise. */ function checkCommandConsent(command: string, args: string[], mainWindow: BrowserWindow): boolean { - const settings = loadSettings(); + const settings = loadSettings(SETTINGS_PATH); const confirmedCommands = settings?.confirmedCommands; // Build the consent key: command + (first arg if present) @@ -121,7 +97,7 @@ function checkCommandConsent(command: string, args: string[], mainWindow: Browse settings.confirmedCommands = {}; } settings.confirmedCommands[consentKey] = commandChoice; - saveSettings(settings); + saveSettings(SETTINGS_PATH, settings); } return true; } @@ -150,7 +126,7 @@ const COMMANDS_WITH_CONSENT = { * @param pluginInfo artifacthub plugin info */ export function addRunCmdConsent(pluginInfo: { name: string }): void { - const settings = loadSettings(); + const settings = loadSettings(SETTINGS_PATH); if (!settings.confirmedCommands) { settings.confirmedCommands = {}; } @@ -169,7 +145,7 @@ export function addRunCmdConsent(pluginInfo: { name: string }): void { } } - saveSettings(settings); + saveSettings(SETTINGS_PATH, settings); } /** @@ -178,7 +154,7 @@ export function addRunCmdConsent(pluginInfo: { name: string }): void { * @param pluginName The package.json name of the plugin. */ export function removeRunCmdConsent(pluginName: string): void { - const settings = loadSettings(); + const settings = loadSettings(SETTINGS_PATH); if (!settings.confirmedCommands) { return; } @@ -193,7 +169,7 @@ export function removeRunCmdConsent(pluginName: string): void { delete settings.confirmedCommands[command]; } - saveSettings(settings); + saveSettings(SETTINGS_PATH, settings); } /** diff --git a/app/electron/settings.test.ts b/app/electron/settings.test.ts new file mode 100644 index 00000000000..eeaa332812e --- /dev/null +++ b/app/electron/settings.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'path'; +import { loadSettings, saveSettings } from './settings'; + +function tmpPath(): string { + return path.join(os.tmpdir(), `settings-test-${Date.now()}-${Math.random()}.json`); +} + +describe('settings load/save', () => { + let filePath: string; + + beforeEach(() => { + filePath = tmpPath(); + try { + if (fs.existsSync(filePath)) fs.unlinkSync(filePath); + } catch { + // ignore + } + }); + + afterEach(() => { + try { + if (fs.existsSync(filePath)) fs.unlinkSync(filePath); + } catch { + // ignore + } + }); + + it('loadSettings returns {} when file does not exist', () => { + expect(fs.existsSync(filePath)).toBe(false); + const res = loadSettings(filePath); + expect(res).toEqual({}); + }); + + it('saveSettings writes JSON and loadSettings reads it back', () => { + const obj = { a: 1, b: 'two', nested: { ok: true } }; + saveSettings(filePath, obj); + expect(fs.existsSync(filePath)).toBe(true); + + const raw = fs.readFileSync(filePath, 'utf-8'); + expect(raw).toBe(JSON.stringify(obj)); + + const loaded = loadSettings(filePath); + expect(loaded).toEqual(obj); + }); + + it('loadSettings returns {} for invalid JSON content', () => { + fs.writeFileSync(filePath, 'not-a-json', 'utf-8'); + const res = loadSettings(filePath); + expect(res).toEqual({}); + }); + + it('loadSettings returns {} when reading a directory path (read error)', () => { + // point to a directory to force read error + const dirPath = os.tmpdir(); + const res = loadSettings(dirPath); + expect(res).toEqual({}); + }); +}); diff --git a/app/electron/settings.ts b/app/electron/settings.ts new file mode 100644 index 00000000000..96768d0c63b --- /dev/null +++ b/app/electron/settings.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from 'electron'; +import fs from 'node:fs'; +import path from 'path'; + +export const SETTINGS_PATH = path.join(app?.getPath('userData') || 'testing', 'settings.json'); + +/** + * Loads the user settings. + * If the settings file does not exist, an empty object is returned. + * @returns The settings object. + */ +export function loadSettings(settingsPath: string): Record { + try { + const data = fs.readFileSync(settingsPath, 'utf-8'); + return JSON.parse(data); + } catch (error) { + return {}; + } +} + +/** + * Saves the user settings. + * @param settings - The settings object to save. + */ +export function saveSettings(settingsPath: string, settings: Record) { + fs.writeFileSync(settingsPath, JSON.stringify(settings), 'utf-8'); +} diff --git a/app/package.json b/app/package.json index e4ad5aa5753..e2b79ea4379 100644 --- a/app/package.json +++ b/app/package.json @@ -125,7 +125,8 @@ "electron/plugin-management.js", "electron/runCmd.js", "electron/mcp/MCPClient.js", - "electron/mcp/MCPToolStateStore.js" + "electron/mcp/MCPToolStateStore.js", + "electron/settings.js" ], "extraResources": [ { From b88d6fffbb77b11a66ebfa81448bcce1b5df894f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Wed, 5 Nov 2025 20:06:42 -0300 Subject: [PATCH 10/20] app: MCPSettings: Add functions for load/save of mcp settings Co-Authored-By: Ashu Ghildiyal --- app/.gitignore | 2 + app/electron/mcp/MCPSettings.test.ts | 67 +++++++++++++++++++++++ app/electron/mcp/MCPSettings.ts | 79 ++++++++++++++++++++++++++++ app/package.json | 3 +- 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 app/electron/mcp/MCPSettings.test.ts create mode 100644 app/electron/mcp/MCPSettings.ts diff --git a/app/.gitignore b/app/.gitignore index 68a2301d54a..28bf77de052 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -19,3 +19,5 @@ electron/mcp/MCPToolStateStore.js electron/mcp/MCPToolStateStore.test.js electron/settings.js electron/settings.test.js +electron/mcp/MCPSettings.js +electron/mcp/MCPSettings.test.js diff --git a/app/electron/mcp/MCPSettings.test.ts b/app/electron/mcp/MCPSettings.test.ts new file mode 100644 index 00000000000..1a46890a826 --- /dev/null +++ b/app/electron/mcp/MCPSettings.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loadSettings, saveSettings } from '../settings'; +import { loadMCPSettings, saveMCPSettings } from './MCPSettings'; + +jest.mock('../settings', () => ({ + loadSettings: jest.fn(), + saveSettings: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('MCPSettings', () => { + it('loadMCPSettings returns mcp settings when present', () => { + const expected = { + enabled: true, + servers: [{ name: 's1', command: 'cmd', args: ['-v'], enabled: true }], + }; + (loadSettings as jest.Mock).mockReturnValue({ mcp: expected }); + + const result = loadMCPSettings('/path/to/settings.json'); + + expect(loadSettings).toHaveBeenCalledWith('/path/to/settings.json'); + expect(result).toEqual(expected); + }); + + it('loadMCPSettings returns null when no mcp settings', () => { + (loadSettings as jest.Mock).mockReturnValue({ other: 123 }); + + const result = loadMCPSettings('/settings'); + + expect(loadSettings).toHaveBeenCalledWith('/settings'); + expect(result).toBeNull(); + }); + + it('saveMCPSettings sets mcp on loaded settings and calls saveSettings', () => { + const existing = { someKey: 'value' }; + (loadSettings as jest.Mock).mockReturnValue(existing); + + const newMCP = { + enabled: false, + servers: [{ name: 's', command: 'c', args: [], enabled: false }], + }; + + saveMCPSettings('/cfg', newMCP); + + expect(loadSettings).toHaveBeenCalledWith('/cfg'); + expect((existing as any).mcp).toBe(newMCP); + expect(saveSettings).toHaveBeenCalledWith('/cfg', existing); + }); +}); diff --git a/app/electron/mcp/MCPSettings.ts b/app/electron/mcp/MCPSettings.ts new file mode 100644 index 00000000000..68d8ef6024b --- /dev/null +++ b/app/electron/mcp/MCPSettings.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loadSettings, saveSettings } from '../settings'; + +interface MCPSettings { + /** + * Whether MCP is enabled or not + */ + enabled: boolean; + /** + * List of MCP servers + */ + servers: MCPServer[]; +} + +interface MCPServer { + /** + * Server name + */ + name: string; + /** + * Command to run the MCP tool + */ + command: string; + /** + * Arguments for the MCP tool command + */ + args: string[]; + /** + * Whether the MCP server is enabled or not + */ + enabled: boolean; + /** + * Environment variables for the MCP tool command + */ + env?: Record; +} + +/** + * Load MCP server configuration from settings + * + * @param settingsPath - path to settings file + * @returns MCP settings or null if not found + */ +export function loadMCPSettings(settingsPath: string): MCPSettings | null { + const settings = loadSettings(settingsPath); + if (!settings || typeof settings !== 'object') { + return null; + } + + const mcp = (settings as any).mcp; + return mcp ? (mcp as MCPSettings) : null; +} + +/** + * Save MCP server configuration to settings + * + * @param settingsPath - path to settings file + * @param mcpSettings - MCP settings to save + */ +export function saveMCPSettings(settingsPath: string, mcpSettings: MCPSettings): void { + const settings = loadSettings(settingsPath); + settings.mcp = mcpSettings; + saveSettings(settingsPath, settings); +} diff --git a/app/package.json b/app/package.json index e2b79ea4379..87084a45bd7 100644 --- a/app/package.json +++ b/app/package.json @@ -126,7 +126,8 @@ "electron/runCmd.js", "electron/mcp/MCPClient.js", "electron/mcp/MCPToolStateStore.js", - "electron/settings.js" + "electron/settings.js", + "electron/mcp/MCPSettings.js" ], "extraResources": [ { From 22ebb224ee65f93b623743b0e1a79c083f537456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Wed, 5 Nov 2025 20:33:56 -0300 Subject: [PATCH 11/20] app: MCPSettings: Add expandEnvAndResolvePaths to process env So that the tools can get expanded environment variables and paths. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPSettings.test.ts | 68 +++++++++++++++++++++++++++- app/electron/mcp/MCPSettings.ts | 64 ++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/app/electron/mcp/MCPSettings.test.ts b/app/electron/mcp/MCPSettings.test.ts index 1a46890a826..94446b18090 100644 --- a/app/electron/mcp/MCPSettings.test.ts +++ b/app/electron/mcp/MCPSettings.test.ts @@ -15,7 +15,7 @@ */ import { loadSettings, saveSettings } from '../settings'; -import { loadMCPSettings, saveMCPSettings } from './MCPSettings'; +import { expandEnvAndResolvePaths, loadMCPSettings, saveMCPSettings } from './MCPSettings'; jest.mock('../settings', () => ({ loadSettings: jest.fn(), @@ -65,3 +65,69 @@ describe('MCPSettings', () => { expect(saveSettings).toHaveBeenCalledWith('/cfg', existing); }); }); + +describe('expandEnvAndResolvePaths', () => { + beforeEach(() => { + // Ensure predictable environment vars + process.env.APPDATA = process.env.APPDATA || ''; + process.env.LOCALAPPDATA = process.env.LOCALAPPDATA || ''; + }); + + it('replaces HEADLAMP_CURRENT_CLUSTER with cluster', () => { + const result = expandEnvAndResolvePaths(['connect HEADLAMP_CURRENT_CLUSTER'], 'my-current'); + expect(result).toEqual(['connect my-current']); + }); + + it('replaces %APPDATA% and %LOCALAPPDATA% with environment values', () => { + process.env.APPDATA = '/some/appdata'; + process.env.LOCALAPPDATA = '/some/localappdata'; + + const result = expandEnvAndResolvePaths(['%APPDATA%/file', '%LOCALAPPDATA%\\other']); + + if (process.platform === 'win32') { + expect(result).toEqual(['/some/appdata/file', '/some/localappdata/other']); + } else { + // on non-windows we expect backslashes to be preserved here + expect(result).toEqual(['/some/appdata/file', '/some/localappdata\\other']); + } + }); + + it('converts backslashes to forward slashes on win32', () => { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'win32' }); + try { + const result = expandEnvAndResolvePaths(['C:\\path\\to\\file', 'nochange/needed']); + expect(result).toEqual(['C:/path/to/file', 'nochange/needed']); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + } + }); + + it('handles docker bind src path conversion on Windows', () => { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'win32' }); + try { + const arg = 'type=bind,src=C:\\path\\to\\dir,dst=/data'; + const result = expandEnvAndResolvePaths([arg]); + // allow a possible current-working-directory prefix (seen on some environments), + // but ensure the drive letter path was converted to /c/path/to/dir or kept as C:/path/to/dir + expect(result[0]).toMatch( + /type=bind,src=(?:.*(?:\/c\/path\/to\/dir|\/[A-Za-z]:\/path\/to\/dir)),dst=\/data/ + ); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + } + }); + + it('does not alter docker bind src path on non-Windows', () => { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'linux' }); + try { + const arg = 'type=bind,src=/home/user/dir,dst=/data'; + const result = expandEnvAndResolvePaths([arg]); + expect(result).toEqual([arg]); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + } + }); +}); diff --git a/app/electron/mcp/MCPSettings.ts b/app/electron/mcp/MCPSettings.ts index 68d8ef6024b..dd72b060348 100644 --- a/app/electron/mcp/MCPSettings.ts +++ b/app/electron/mcp/MCPSettings.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import os from 'os'; +import path from 'path'; import { loadSettings, saveSettings } from '../settings'; interface MCPSettings { @@ -77,3 +79,65 @@ export function saveMCPSettings(settingsPath: string, mcpSettings: MCPSettings): settings.mcp = mcpSettings; saveSettings(settingsPath, settings); } + +/** + * Expand environment variables and resolve paths in arguments. + * + * @param args - The array of argument strings to expand. + * @param currentCluster - The current cluster name to replace HEADLAMP_CURRENT_CLUSTER. + * @param cluster - The specific cluster name to replace HEADLAMP_CURRENT_CLUSTER, if provided. + * + * @returns The array of expanded argument strings. + */ +export function expandEnvAndResolvePaths(args: string[], cluster: string | null = null): string[] { + return args.map(arg => { + // Replace Windows environment variables like %USERPROFILE% + let expandedArg = arg; + + // Handle HEADLAMP_CURRENT_CLUSTER placeholder + if (expandedArg.includes('HEADLAMP_CURRENT_CLUSTER')) { + expandedArg = expandedArg.replace(/HEADLAMP_CURRENT_CLUSTER/g, cluster || ''); + } + + // Handle %USERPROFILE% + if (expandedArg.includes('%USERPROFILE%')) { + expandedArg = expandedArg.replace(/%USERPROFILE%/g, os.homedir()); + } + + // Handle other common Windows environment variables + if (expandedArg.includes('%APPDATA%')) { + expandedArg = expandedArg.replace(/%APPDATA%/g, process.env.APPDATA || ''); + } + + if (expandedArg.includes('%LOCALAPPDATA%')) { + expandedArg = expandedArg.replace(/%LOCALAPPDATA%/g, process.env.LOCALAPPDATA || ''); + } + + // Convert Windows backslashes to forward slashes for Docker + if (process.platform === 'win32' && expandedArg.includes('\\')) { + expandedArg = expandedArg.replace(/\\/g, '/'); + } + + // Handle Docker volume mount format and ensure proper Windows path format + if (expandedArg.includes('type=bind,src=')) { + const match = expandedArg.match(/type=bind,src=(.+?),dst=(.+)/); + if (match) { + let srcPath = match[1]; + const dstPath = match[2]; + + // Resolve the source path + if (process.platform === 'win32') { + srcPath = path.resolve(srcPath); + // For Docker on Windows, we might need to convert C:\ to /c/ format + if (srcPath.match(/^[A-Za-z]:/)) { + srcPath = '/' + srcPath.charAt(0).toLowerCase() + srcPath.slice(2).replace(/\\/g, '/'); + } + } + + expandedArg = `type=bind,src=${srcPath},dst=${dstPath}`; + } + } + + return expandedArg; + }); +} From a2e8345d00c51bd3cbd02556b8233e559cba7eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Wed, 5 Nov 2025 21:18:28 -0300 Subject: [PATCH 12/20] app: MCPSettings: Add makeMcpServersFromSettings Make mpcServers from settings for the mpcServers arg of MultiServerMCPClient. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPSettings.test.ts | 110 +++++++++++++++++++++++++++ app/electron/mcp/MCPSettings.ts | 56 ++++++++++++++ 2 files changed, 166 insertions(+) diff --git a/app/electron/mcp/MCPSettings.test.ts b/app/electron/mcp/MCPSettings.test.ts index 94446b18090..4127c407357 100644 --- a/app/electron/mcp/MCPSettings.test.ts +++ b/app/electron/mcp/MCPSettings.test.ts @@ -16,6 +16,7 @@ import { loadSettings, saveSettings } from '../settings'; import { expandEnvAndResolvePaths, loadMCPSettings, saveMCPSettings } from './MCPSettings'; +import * as MCP from './MCPSettings'; jest.mock('../settings', () => ({ loadSettings: jest.fn(), @@ -131,3 +132,112 @@ describe('expandEnvAndResolvePaths', () => { } }); }); + +describe('MultiServerMCPClient', () => { + beforeEach(() => { + // ensure predictable env for merging tests + process.env.TEST_ORIG_ENV = 'orig'; + jest.resetAllMocks(); + }); + + afterEach(() => { + delete process.env.TEST_ORIG_ENV; + }); + + it('returns empty when no mcp settings', () => { + jest.spyOn(MCP, 'loadMCPSettings').mockReturnValue(null); + + const result = MCP.makeMcpServersFromSettings('/cfg', ['cluster1']); + + // Cannot reliably assert the internal call to loadMCPSettings when both functions + // are in the same module (jest.spyOn does not intercept internal local references), + // so only assert the returned result. + expect(result).toEqual({}); + }); + + it('returns empty when mcp is disabled or has no servers', () => { + jest.spyOn(MCP, 'loadMCPSettings').mockReturnValue({ enabled: false, servers: [] }); + expect(MCP.makeMcpServersFromSettings('/cfg', ['c'])).toEqual({}); + + jest.spyOn(MCP, 'loadMCPSettings').mockReturnValue({ enabled: true, servers: [] }); + expect(MCP.makeMcpServersFromSettings('/cfg', ['c'])).toEqual({}); + }); + + it('filters out disabled or invalid servers and builds server entries', () => { + const mcpSettings = { + enabled: true, + servers: [ + { + name: 'valid', + command: 'cmd', + args: ['arg1'], + enabled: true, + env: { MCP_VAR: 'mcp' }, + }, + { + name: 'disabled', + command: 'cmd', + args: [], + enabled: false, + }, + { + // missing command + name: 'nocmd', + command: '', + args: [], + enabled: true, + }, + { + // missing name + name: '', + command: 'cmd', + args: [], + enabled: true, + }, + ], + }; + + (loadSettings as jest.Mock).mockReturnValue({ mcp: mcpSettings }); + + const result = MCP.makeMcpServersFromSettings('/cfg', ['clusterA']); + + expect(result).toHaveProperty('valid'); + expect(Object.keys(result)).toEqual(['valid']); + + const entry = result['valid'] as any; + expect(entry.transport).toBe('stdio'); + expect(entry.command).toBe('cmd'); + expect(entry.args).toEqual(['arg1']); + // env should include process.env and server.env overrides + expect(entry.env.MCP_VAR).toBe('mcp'); + expect(entry.env.TEST_ORIG_ENV).toBe('orig'); + // restart settings + expect(entry.restart).toBeDefined(); + expect(entry.restart.enabled).toBe(true); + expect(entry.restart.maxAttempts).toBe(3); + expect(entry.restart.delayMs).toBe(2000); + }); + + it('expands HEADLAMP_CURRENT_CLUSTER placeholder using provided clusters[0]', () => { + const mcpSettings = { + enabled: true, + servers: [ + { + name: 'withCluster', + command: 'cmd', + args: ['connect', 'HEADLAMP_CURRENT_CLUSTER'], + enabled: true, + }, + ], + }; + + (loadSettings as jest.Mock).mockReturnValue({ mcp: mcpSettings }); + + const result = MCP.makeMcpServersFromSettings('/cfg', ['my-current-cluster']); + + expect(result).toHaveProperty('withCluster'); + const entry = result['withCluster'] as any; + // the expand function should have replaced the placeholder + expect(entry.args).toEqual(['connect', 'my-current-cluster']); + }); +}); diff --git a/app/electron/mcp/MCPSettings.ts b/app/electron/mcp/MCPSettings.ts index dd72b060348..01eda46ed20 100644 --- a/app/electron/mcp/MCPSettings.ts +++ b/app/electron/mcp/MCPSettings.ts @@ -14,10 +14,13 @@ * limitations under the License. */ +import type { ClientConfig } from '@langchain/mcp-adapters'; import os from 'os'; import path from 'path'; import { loadSettings, saveSettings } from '../settings'; +const DEBUG = true; + interface MCPSettings { /** * Whether MCP is enabled or not @@ -141,3 +144,56 @@ export function expandEnvAndResolvePaths(args: string[], cluster: string | null return expandedArg; }); } + +/** + * Make mpcServers from settings for the mpcServers arg of MultiServerMCPClient. + * + * @param settingsPath - path to settings file + * @param clusters - list of current clusters + * + * @returns Record of MCP servers + */ +export function makeMcpServersFromSettings( + settingsPath: string, + clusters: string[] +): ClientConfig['mcpServers'] { + const mcpServers: ClientConfig['mcpServers'] = {}; + + const mcpSettings = loadMCPSettings(settingsPath); + if ( + !mcpSettings || + !mcpSettings.enabled || + !mcpSettings.servers || + mcpSettings.servers.length === 0 + ) { + return mcpServers; + } + + for (const server of mcpSettings.servers) { + if (!server.enabled || !server.name || !server.command) { + continue; + } + + const expandedArgs = expandEnvAndResolvePaths(server.args || [], clusters[0] || null); + + if (DEBUG) { + console.log(`Expanded args for ${server.name}:`, expandedArgs); + } + + const serverEnv = server.env ? { ...process.env, ...server.env } : process.env; + + mcpServers[server.name] = { + transport: 'stdio', + command: server.command, + args: expandedArgs, + env: serverEnv as Record, + restart: { + enabled: true, + maxAttempts: 3, + delayMs: 2000, + }, + }; + } + + return mcpServers; +} From 35a76e6fd40a1e4c92e04399171b00ac24fc582e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 6 Nov 2025 06:08:35 -0300 Subject: [PATCH 13/20] app: MCPSettings: Add settingsChanges for human-readable changes settingsChanges returns a list of human-readable descriptions of changes between the current settings and the next settings. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPSettings.test.ts | 92 ++++++++++++++++++++++++++++ app/electron/mcp/MCPSettings.ts | 83 +++++++++++++++++++++++++ 2 files changed, 175 insertions(+) diff --git a/app/electron/mcp/MCPSettings.test.ts b/app/electron/mcp/MCPSettings.test.ts index 4127c407357..9bb53799810 100644 --- a/app/electron/mcp/MCPSettings.test.ts +++ b/app/electron/mcp/MCPSettings.test.ts @@ -241,3 +241,95 @@ describe('MultiServerMCPClient', () => { expect(entry.args).toEqual(['connect', 'my-current-cluster']); }); }); + +describe('settingsChanges', () => { + it('reports enabling and added servers when current is null', () => { + const nextSettings = { + enabled: true, + servers: [{ name: 's1', command: 'cmd1', args: [], enabled: true }], + }; + + const result = MCP.settingsChanges(null, nextSettings as any); + expect(result).toContain('• MCP will be ENABLED'); + expect(result).toContain('• ADD server: "s1" (cmd1)'); + }); + + it('returns empty array when both current and next settings are null', () => { + const result = MCP.settingsChanges(null, null as any); + expect(result).toEqual([]); + }); + + it('reports disabling and removed servers when next is null', () => { + const current = { + enabled: true, + servers: [ + { name: 's1', command: 'cmd1', args: [], enabled: true }, + { name: 's2', command: 'cmd2', args: [], enabled: true }, + ], + }; + + const result = MCP.settingsChanges(current as any, null as any); + expect(result).toContain('• MCP will be DISABLED'); + expect(result).toContain('• REMOVE server: "s1"'); + expect(result).toContain('• REMOVE server: "s2"'); + }); + + it('reports disabling when enabled -> disabled and no servers', () => { + const current = { enabled: true, servers: [] }; + const next = { enabled: false, servers: [] }; + + const result = MCP.settingsChanges(current as any, next as any); + expect(result).toEqual(['• MCP will be DISABLED']); + }); + + it('detects added, removed and modified servers including command/args/env/enable changes', () => { + const current = { + enabled: true, + servers: [ + { name: 'keep', command: 'cmd', args: ['a'], enabled: true, env: { X: '1' } }, + { name: 'removed', command: 'rm', args: [], enabled: true }, + { name: 'modified', command: 'old', args: ['one'], enabled: true, env: { A: 'a' } }, + ], + }; + + const next = { + enabled: true, + servers: [ + { name: 'keep', command: 'cmd', args: ['a'], enabled: true, env: { X: '1' } }, // unchanged + { name: 'added', command: 'new', args: [], enabled: true }, // new + { + name: 'modified', + command: 'newcmd', + args: ['one', 'two'], + enabled: false, // toggled + env: { A: 'b' }, // changed + }, + ], + }; + + const result = MCP.settingsChanges(current as any, next as any); + + expect(result).toEqual( + expect.arrayContaining(['• ADD server: "added" (new)', '• REMOVE server: "removed"']) + ); + + // find the modify message for 'modified' server + const modifyMsg = result.find(r => r.startsWith('• MODIFY server "modified"')); + expect(modifyMsg).toBeDefined(); + // should mention enable/disable, command change, args change, and env change + expect(modifyMsg).toMatch(/enable|disable/); + expect(modifyMsg).toMatch(/change command: "old" → "newcmd"/); + expect(modifyMsg).toMatch(/change arguments: \["one"\] → \["one","two"\]/); + expect(modifyMsg).toMatch(/change environment variables/); + }); + + it('returns empty array when there are no changes', () => { + const s = { + enabled: true, + servers: [{ name: 's', command: 'c', args: ['x'], enabled: true, env: { K: 'v' } }], + }; + + const result = MCP.settingsChanges(s as any, s as any); + expect(result).toEqual([]); + }); +}); diff --git a/app/electron/mcp/MCPSettings.ts b/app/electron/mcp/MCPSettings.ts index 01eda46ed20..29846df1951 100644 --- a/app/electron/mcp/MCPSettings.ts +++ b/app/electron/mcp/MCPSettings.ts @@ -197,3 +197,86 @@ export function makeMcpServersFromSettings( return mcpServers; } + +/** + * settingsChanges returns a list of human-readable descriptions of changes + * between the current MCP settings and next MCP settings. + * + * @param currentSettings - The current MCP settings, or null if none exist. + * @param nextSettings - The next MCP settings to compare against. + * + * @returns An array of strings describing the changes. + */ +export function settingsChanges( + currentSettings: MCPSettings | null, + nextSettings: MCPSettings | null +): string[] { + const changes: string[] = []; + + // Check if MCP is being enabled/disabled + const currentEnabled = currentSettings?.enabled ?? false; + const nextEnabled = nextSettings?.enabled ?? false; + + if (currentEnabled !== nextEnabled) { + changes.push(`• MCP will be ${nextEnabled ? 'ENABLED' : 'DISABLED'}`); + } + + // Get current and next server lists + const currentServers = currentSettings?.servers ?? []; + const nextServers = nextSettings?.servers ?? []; + + // Check for added servers + const currentServerNames = new Set(currentServers.map(s => s.name)); + const nextServerNames = new Set(nextServers.map(s => s.name)); + + for (const server of nextServers) { + if (!currentServerNames.has(server.name)) { + changes.push(`• ADD server: "${server.name}" (${server.command})`); + } + } + + // Check for removed servers + for (const server of currentServers) { + if (!nextServerNames.has(server.name)) { + changes.push(`• REMOVE server: "${server.name}"`); + } + } + + // Check for modified servers + for (const nextServer of nextServers) { + const currentServer = currentServers.find(s => s.name === nextServer.name); + if (currentServer) { + const serverChanges: string[] = []; + + // Check enabled status + if (currentServer.enabled !== nextServer.enabled) { + serverChanges.push(`${nextServer.enabled ? 'enable' : 'disable'}`); + } + + // Check command + if (currentServer.command !== nextServer.command) { + serverChanges.push(`change command: "${currentServer.command}" → "${nextServer.command}"`); + } + + // Check arguments + const currentArgs = JSON.stringify(currentServer.args || []); + const nextArgs = JSON.stringify(nextServer.args || []); + if (currentArgs !== nextArgs) { + serverChanges.push(`change arguments: ${currentArgs} → ${nextArgs}`); + } + + // Check environment variables + const currentEnv = JSON.stringify(currentServer.env || {}); + const nextEnv = JSON.stringify(nextServer.env || {}); + if (currentEnv !== nextEnv) { + serverChanges.push(`change environment variables`); + } + + if (serverChanges.length > 0) { + changes.push(`• MODIFY server "${nextServer.name}": ${serverChanges.join(', ')}`); + } + } + } + + return changes; +} From a9fb6b1f51d6c115d67cd903ce1e1e0c75c36e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 6 Nov 2025 12:19:01 -0300 Subject: [PATCH 14/20] app: MCPSettings: Add showSettingsChangeDialog Shows a dialog asking user for confirmation if MCP settings changes are ok. Displays a summary of changes between currentSettings and nextSettings. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPSettings.ts | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/app/electron/mcp/MCPSettings.ts b/app/electron/mcp/MCPSettings.ts index 29846df1951..107ff7e76de 100644 --- a/app/electron/mcp/MCPSettings.ts +++ b/app/electron/mcp/MCPSettings.ts @@ -15,6 +15,7 @@ */ import type { ClientConfig } from '@langchain/mcp-adapters'; +import { type BrowserWindow, dialog } from 'electron'; import os from 'os'; import path from 'path'; import { loadSettings, saveSettings } from '../settings'; @@ -280,3 +281,36 @@ export function settingsChanges( return changes; } + +/** + * Shows a dialog asking user for confirmation if MCP settings changes are ok. + * + * Displays a summary of changes between currentSettings and nextSettings. + * + * @param mainWindow - The main BrowserWindow to parent the dialog. + * @param currentSettings - Current MCP settings, or null if none exists. + * @param nextSettings - New MCP settings to be applied. + * + * @returns Promise resolving to true if user approves changes, false if cancelled. + */ +export async function showSettingsChangeDialog( + mainWindow: BrowserWindow, + currentSettings: MCPSettings | null, + nextSettings: MCPSettings +): Promise { + const changes = settingsChanges(currentSettings, nextSettings); + const result = await dialog.showMessageBox(mainWindow, { + type: 'question', + buttons: ['Apply Changes', 'Cancel'], + defaultId: 1, + title: 'MCP Settings Changes', + message: 'The application wants to update the MCP settings.', + detail: + changes.length > 0 + ? `The following changes will be applied:\n\n${changes.join( + '\n' + )}\n\nDo you want to apply these changes?` + : 'No changes detected in the MCP settings.\n\nDo you want to proceed anyway?', + }); + return result.response === 0; // 0 is "Apply Changes" +} From d3cce2479e1f3a0ba5050bb86bc4d8229b7c92a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 6 Nov 2025 16:08:02 -0300 Subject: [PATCH 15/20] app: MCPSettings: Add hasClusterDependentServers Check if any server in the settings uses HEADLAMP_CURRENT_CLUSTER placeholder. This determines whether the MCP client needs to be restarted on cluster changes. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPSettings.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/electron/mcp/MCPSettings.ts b/app/electron/mcp/MCPSettings.ts index 107ff7e76de..772ed94002d 100644 --- a/app/electron/mcp/MCPSettings.ts +++ b/app/electron/mcp/MCPSettings.ts @@ -314,3 +314,22 @@ export async function showSettingsChangeDialog( }); return result.response === 0; // 0 is "Apply Changes" } + +/** + * Check if any server in the settings uses HEADLAMP_CURRENT_CLUSTER placeholder. + * This determines whether the MCP client needs to be restarted on cluster changes. + * + * @param settingsPath - path to settings file + * + * @returns True if any enabled server has HEADLAMP_CURRENT_CLUSTER in its arguments + */ +export function hasClusterDependentServers(settingsPath: string): boolean { + return ( + loadMCPSettings(settingsPath)?.servers.some( + server => + server.enabled && + server.args && + server.args.some(arg => arg.includes('HEADLAMP_CURRENT_CLUSTER')) + ) || false + ); +} From 02390425b33a4dcfc20c905c8a918de3faa53d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 6 Nov 2025 12:39:35 -0300 Subject: [PATCH 16/20] app: MCPClient: Add showConfirmationDialog Show user confirmation dialog for MCP operations. Displays a dialog to the user for security confirmation before executing MCP operations. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPClient.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/electron/mcp/MCPClient.ts b/app/electron/mcp/MCPClient.ts index 0f1a9a0141b..cddd9afb85b 100644 --- a/app/electron/mcp/MCPClient.ts +++ b/app/electron/mcp/MCPClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { BrowserWindow } from 'electron'; +import { type BrowserWindow, dialog } from 'electron'; import { MCPToolStateStore } from './MCPToolStateStore'; const DEBUG = true; @@ -97,3 +97,29 @@ export default class MCPClient { } } } + +/** + * Show user confirmation dialog for MCP operations. + * Displays a dialog to the user for security confirmation before executing MCP operations. + * + * @param title - Dialog title + * @param message - Main message to display to the user + * @param operation - Description of the operation being performed + * @returns Promise resolving to true if user allows the operation, false otherwise + */ +export async function showConfirmationDialog( + mainWindow: BrowserWindow, + title: string, + message: string, + operation: string +): Promise { + const result = await dialog.showMessageBox(mainWindow, { + type: 'question', + buttons: ['Allow', 'Cancel'], + defaultId: 1, + title, + message, + detail: `Operation: ${operation}\n\nDo you want to allow this MCP operation?`, + }); + return result.response === 0; // 0 is "Allow" +} From 16712c69edfcd40c39196546681a314c8abe286f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 6 Nov 2025 16:51:39 -0300 Subject: [PATCH 17/20] app: main: MCPClient: Use MultiServerMCPClient from langchain/mcp-adapters - MCPClient accepts settingsPath and manages init lifecycle of MultiServerMCPClient - initializeClient and doInitializeClient to prevent duplicate inits - caches clientTools and client instance, tracks isInitialized and init promise - cleanup and close logic for client - constructs MultiServerMCPClient with options and populates MCPToolStateStore - handles cluster changes: no-op when identical, skips restart if no cluster-dependent servers, restarts client when necessary and restores state on error - Add tests for initialization, empty-server handling, tool caching, and cluster-change behaviors. Co-Authored-By: Ashu Ghildiyal --- app/electron/main.ts | 3 +- app/electron/mcp/MCPClient.test.ts | 173 ++++++++++++++++++++++++++- app/electron/mcp/MCPClient.ts | 180 ++++++++++++++++++++++++++--- 3 files changed, 340 insertions(+), 16 deletions(-) diff --git a/app/electron/main.ts b/app/electron/main.ts index dc80acefa29..286151cc1af 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -1610,7 +1610,8 @@ function startElecron() { if (ENABLE_MCP) { const configPath = path.join(app.getPath('userData'), 'mcp-tools-config.json'); - mcpClient = new MCPClient(configPath); + const settingsPath = path.join(app.getPath('userData'), 'mcp-tools-settings.json'); + mcpClient = new MCPClient(configPath, settingsPath); await mcpClient.initialize(); mcpClient.setMainWindow(mainWindow); } diff --git a/app/electron/mcp/MCPClient.test.ts b/app/electron/mcp/MCPClient.test.ts index 3745c665de0..f98330c725d 100644 --- a/app/electron/mcp/MCPClient.test.ts +++ b/app/electron/mcp/MCPClient.test.ts @@ -28,14 +28,21 @@ describe('MCPClient', () => { let infoSpy: jest.Mock; let cfgPath: string; + let settingsPath: string; beforeEach(() => { cfgPath = tmpPath(); + settingsPath = tmpPath(); try { if (fs.existsSync(cfgPath)) fs.unlinkSync(cfgPath); } catch { // ignore } + try { + if (fs.existsSync(settingsPath)) fs.unlinkSync(settingsPath); + } catch { + // ignore + } }); afterEach(() => { @@ -44,10 +51,15 @@ describe('MCPClient', () => { } catch { // ignore } + try { + if (fs.existsSync(settingsPath)) fs.unlinkSync(settingsPath); + } catch { + // ignore + } }); beforeEach(() => { - client = new MCPClient(cfgPath); + client = new MCPClient(cfgPath, settingsPath); // spy on console.info to avoid noisy output and to assert calls infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}) as unknown as jest.Mock; }); @@ -110,6 +122,165 @@ describe('MCPClient', () => { // no cleanup log should be emitted since it early-returns when not initialized expect(infoSpy).not.toHaveBeenCalledWith('MCPClient: cleaned up'); }); + + it('initialize marks isInitialized and leaves client null when no servers are configured', async () => { + // Mock MCPSettings to return no servers + jest.doMock('./MCPSettings', () => ({ + makeMcpServersFromSettings: jest.fn().mockReturnValue({}), + hasClusterDependentServers: jest.fn().mockReturnValue(false), + })); + // Mock MultiServerMCPClient just in case, it should not be constructed + const MultiServerMCPClientMock = jest.fn(); + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: MultiServerMCPClientMock, + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath); + + const infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + await client.initialize(); + + expect((client as any).isInitialized).toBe(true); + expect((client as any).client).toBeNull(); + // ensure the public log happened + expect(infoSpy).toHaveBeenCalledWith('MCPClient: initialized'); + }); + + it('initialize constructs MCP client and caches tools when servers exist', async () => { + const fakeTools = [{ name: 't1' }, { name: 't2' }]; + const getTools = jest.fn().mockResolvedValue(fakeTools); + const close = jest.fn().mockResolvedValue(undefined); + const MultiServerMCPClientMock = jest.fn().mockImplementation(() => ({ getTools, close })); + + jest.resetModules(); + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: MultiServerMCPClientMock, + })); + jest.doMock('./MCPSettings', () => ({ + makeMcpServersFromSettings: jest.fn().mockReturnValue({ serverA: { url: 'http://x' } }), + hasClusterDependentServers: jest.fn().mockReturnValue(false), + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath); + + await client.initialize(); + + // Ensure the mock constructor was called to create the client + expect(MultiServerMCPClientMock).toHaveBeenCalled(); + // Ensure tools were cached + expect((client as any).clientTools).toEqual(fakeTools); + // Ensure MCPToolStateStore was initialized (non-null) + expect((client as any).mcpToolState).not.toBeNull(); + }); + + it('handleClustersChange logs and returns early when no cluster-dependent servers', async () => { + const getTools = jest.fn().mockResolvedValue([]); + const close = jest.fn().mockResolvedValue(undefined); + const MultiServerMCPClientMock = jest.fn().mockImplementation(() => ({ getTools, close })); + + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: MultiServerMCPClientMock, + })); + // make servers exist, but indicate no cluster-dependent servers + const makeMcpServersFromSettings = jest.fn().mockReturnValue({ serverA: {} }); + const hasClusterDependentServers = jest.fn().mockReturnValue(false); + jest.doMock('./MCPSettings', () => ({ + makeMcpServersFromSettings, + hasClusterDependentServers, + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath); + + await client.initialize(); + + const beforeClient = (client as any).client; + + await client.handleClustersChange(['cluster-x']); + + // since hasClusterDependentServers returned false, client unchanged + expect((client as any).client).toBe(beforeClient); + }); + + it('handleClustersChange does nothing when clusters array is identical', async () => { + const getTools = jest.fn().mockResolvedValue([]); + const close = jest.fn().mockResolvedValue(undefined); + const MultiServerMCPClientMock = jest.fn().mockImplementation(() => ({ getTools, close })); + + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: MultiServerMCPClientMock, + })); + jest.doMock('./MCPSettings', () => ({ + makeMcpServersFromSettings: jest.fn().mockReturnValue({ serverA: {} }), + hasClusterDependentServers: jest.fn().mockReturnValue(true), + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath); + + await client.initialize(); + + // set currentClusters to a value and call with the same value + (client as any).currentClusters = ['same-cluster']; + + jest.spyOn(console, 'info').mockImplementation(() => {}); + const closeSpy = jest.spyOn((client as any).client, 'close').mockImplementation(async () => {}); + + await client.handleClustersChange(['same-cluster']); + + // close should not have been called because clusters didn't change + expect(closeSpy).not.toHaveBeenCalled(); + }); + + it('handleClustersChange restarts client when cluster-dependent servers exist', async () => { + const getToolsFirst = jest.fn().mockResolvedValue([{ name: 'a' }]); + const closeFirst = jest.fn().mockResolvedValue(undefined); + const getToolsSecond = jest.fn().mockResolvedValue([{ name: 'b' }]); + const closeSecond = jest.fn().mockResolvedValue(undefined); + + // We'll create a factory to return different instances on subsequent constructions + const instances: any[] = [ + { getTools: getToolsFirst, close: closeFirst }, + { getTools: getToolsSecond, close: closeSecond }, + ]; + const MultiServerMCPClientMock = jest.fn().mockImplementation(() => instances.shift()); + + // Ensure module cache is cleared so our doMock is respected when requiring the MCPClient module + jest.resetModules(); + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: MultiServerMCPClientMock, + })); + const makeMcpServersFromSettings = jest.fn().mockReturnValue({ serverA: {} }); + const hasClusterDependentServers = jest.fn().mockReturnValue(true); + jest.doMock('./MCPSettings', () => ({ + makeMcpServersFromSettings, + hasClusterDependentServers, + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath); + + jest.spyOn(console, 'log').mockImplementation(() => {}); + await client.initialize(); + + // initial client should be the first instance + const firstClientRef = (client as any).client; + expect(firstClientRef).not.toBeNull(); + + // Trigger cluster change: should create a new client for the new cluster + await client.handleClustersChange(['new-cluster']); + + // The MCP client constructor should have been called for initial setup and again for restart + expect(MultiServerMCPClientMock).toHaveBeenCalledTimes(2); + + // After restart, client should have been replaced + const afterClientRef = (client as any).client; + expect(afterClientRef).not.toBeNull(); + // And tools should reflect second initialization + expect((client as any).clientTools).toEqual([{ name: 'b' }]); + }); }); describe('MCPClient logging behavior', () => { diff --git a/app/electron/mcp/MCPClient.ts b/app/electron/mcp/MCPClient.ts index cddd9afb85b..40730214cc0 100644 --- a/app/electron/mcp/MCPClient.ts +++ b/app/electron/mcp/MCPClient.ts @@ -14,7 +14,10 @@ * limitations under the License. */ +import type { DynamicStructuredTool } from '@langchain/core/dist/tools/index'; +import { MultiServerMCPClient } from '@langchain/mcp-adapters'; import { type BrowserWindow, dialog } from 'electron'; +import { hasClusterDependentServers, makeMcpServersFromSettings } from './MCPSettings'; import { MCPToolStateStore } from './MCPToolStateStore'; const DEBUG = true; @@ -28,7 +31,9 @@ const DEBUG = true; * Example: * ```ts * const configPath = path.join(app.getPath('userData'), 'mcp-tools-config.json'); - * const mcpClient = new MCPClient(configPath); + * const mainWindow = new BrowserWindow({ ... }); + * const settingsPath = path.join(app.getPath('userData'), 'settings.json'); + * const mcpClient = new MCPClient(configPath, settingsPath); * await mcpClient.initialize(); * mcpClient.setMainWindow(mainWindow); * await mcpClient.handleClustersChange(['cluster-1']); @@ -41,8 +46,24 @@ export default class MCPClient { private mcpToolState: MCPToolStateStore | null = null; private readonly configPath: string; - constructor(configPath: string) { + /** Cached list of available tools from all MCP servers */ + private clientTools: DynamicStructuredTool[] = []; + /** The LangChain MCP client instance managing multiple servers */ + private client: MultiServerMCPClient | null = null; + /** Whether the MCP client has been successfully initialized */ + private isInitialized = false; + /** Promise tracking ongoing initialization to prevent duplicate initializations */ + private initializationPromise: Promise | null = null; + + private settingsPath: string; + private clusters: string[] = []; + + private currentClusters: string[] | null = null; + private oldClusters: string[] | null = null; + + constructor(configPath: string, settingsPath: string) { this.configPath = configPath; + this.settingsPath = settingsPath; } /** @@ -54,32 +75,92 @@ export default class MCPClient { } this.mcpToolState = new MCPToolStateStore(this.configPath); + await this.initializeClient(); + this.initialized = true; + if (DEBUG) { console.info('MCPClient: initialized'); } } /** - * Set the main BrowserWindow for IPC notifications. + * Initialize the MCP client if not already initialized. * - * @param win - The main BrowserWindow instance, or null to clear it. + * @return Promise that resolves when initialization is complete. */ - setMainWindow(win: BrowserWindow | null): void { - this.mainWindow = win; + private async initializeClient(): Promise { + if (DEBUG) { + console.log('MCPClient: initializeClient: ', { + isInitialized: this.isInitialized, + initializationPromise: this.initializationPromise, + }); + } + + if (this.isInitialized) { + return; + } + if (this.initializationPromise) { + return this.initializationPromise; + } + + if (DEBUG) { + console.log('MCPClient: initializeClient: Starting doInitialize()...'); + } + + this.initializationPromise = this.doInitializeClient(); + return this.initializationPromise; } /** - * Handle clusters change notification. + * Perform the actual initialization of the MCP client. * - * @param clusters - The new active clusters array, or null if none. + * @throws {Error} If initialization fails */ - async handleClustersChange(clusters: string[] | null): Promise { - if (DEBUG) { - console.info('MCPClient: clusters changed ->', clusters); - } - if (!this.initialized) { - throw new Error('MCPClient: not initialized'); + private async doInitializeClient(): Promise { + try { + const mcpServers = makeMcpServersFromSettings(this.settingsPath, this.clusters); + + // If no enabled servers, skip initialization + if (Object.keys(mcpServers).length === 0) { + if (DEBUG) { + console.log('MCPClient: doInitialize: No enabled MCP servers found'); + } + this.isInitialized = true; + return; + } + if (DEBUG) { + console.log( + 'MCPClient: doInitialize: Initializing MCP client with servers:', + Object.keys(mcpServers) + ); + } + this.client = new MultiServerMCPClient({ + throwOnLoadError: false, // Don't throw on load error to allow partial initialization + prefixToolNameWithServerName: true, // Prefix to avoid name conflicts + additionalToolNamePrefix: '', + useStandardContentBlocks: true, + mcpServers, + defaultToolTimeout: 2 * 60 * 1000, // 2 minutes + }); + // Get and cache the tools + this.clientTools = await this.client.getTools(); + this.mcpToolState?.initConfigFromClientTools(this.clientTools); + + this.isInitialized = true; + if (DEBUG) { + console.log( + 'MCPClient: doInitialize: MCP client initialized successfully with', + this.clientTools.length, + 'tools' + ); + } + } catch (error) { + console.error('Failed to initialize MCP client:', error); + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + throw error; } } @@ -92,10 +173,81 @@ export default class MCPClient { } this.mainWindow = null; this.initialized = false; + + if (this.client) { + try { + await this.client.close(); + } catch (error) { + console.error('Error cleaning up MCP client:', error); + } + } + this.client = null; + this.clientTools = []; + this.isInitialized = false; + this.initializationPromise = null; + if (DEBUG) { console.info('MCPClient: cleaned up'); } } + + /** + * Set the main BrowserWindow for IPC notifications. + * + * @param win - The main BrowserWindow instance, or null to clear it. + */ + setMainWindow(win: BrowserWindow | null): void { + this.mainWindow = win; + } + + /** + * Handle clusters change notification. + * + * @param clusters - The new active clusters array, or null if none. + */ + async handleClustersChange(newClusters: string[] | null): Promise { + if (DEBUG) { + console.info('MCPClient: clusters changed ->', newClusters); + } + + if (!this.initialized) { + throw new Error('MCPClient: not initialized'); + } + + // If cluster hasn't actually changed, do nothing. + if (JSON.stringify(this.currentClusters) === JSON.stringify(newClusters)) { + return; + } + + const oldClusters = this.currentClusters; + this.currentClusters = newClusters; + + // Check if we have any cluster-dependent servers + if (!hasClusterDependentServers(this.settingsPath)) { + console.log('No cluster-dependent MCP servers found, skipping restart'); + return; + } + + try { + // Reset the client + if (this.client) { + if (typeof (this.client as any).close === 'function') { + await (this.client as any).close(); + } + } + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + // Re-initialize with new cluster context + await this.initializeClient(); + console.log('MCP client restarted successfully for new cluster:', newClusters); + } catch (error) { + console.error('Error restarting MCP client for cluster change:', error); + // Restore previous cluster on error + this.currentClusters = oldClusters; + throw error; + } + } } /** From e79685b4574d77a104c5d2b0060791f6288c5d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 6 Nov 2025 21:19:29 -0300 Subject: [PATCH 18/20] app: MCPClient: Add mcpExecuteTool Implement a private mcpExecuteTool on MCPClient that: - parses server/tool names via MCPToolStateStore helpers, - validates arguments with validateToolArgs, - checks whether the tool is enabled, - invokes the tool and records usage via mcpToolState, - returns a structured success/error result (including toolCallId). This will be used via Electron ipc from the frontend/. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPClient.test.ts | 163 +++++++++++++++++++++++++++++ app/electron/mcp/MCPClient.ts | 60 ++++++++++- 2 files changed, 222 insertions(+), 1 deletion(-) diff --git a/app/electron/mcp/MCPClient.test.ts b/app/electron/mcp/MCPClient.test.ts index f98330c725d..71750fd64c6 100644 --- a/app/electron/mcp/MCPClient.test.ts +++ b/app/electron/mcp/MCPClient.test.ts @@ -301,3 +301,166 @@ describe('MCPClient logging behavior', () => { infoSpy.mockRestore(); }); }); + +describe('MCPClient#mcpExecuteTool', () => { + const cfgPath = tmpPath(); + const settingsPath = tmpPath(); + + beforeEach(() => { + try { + if (fs.existsSync(cfgPath)) fs.unlinkSync(cfgPath); + } catch {} + try { + if (fs.existsSync(settingsPath)) fs.unlinkSync(settingsPath); + } catch {} + }); + + it('executes a tool successfully and records usage', async () => { + jest.resetModules(); + jest.doMock('./MCPToolStateStore', () => ({ + parseServerNameToolName: jest.fn().mockImplementation((fullName: string) => { + const [serverName, ...rest] = fullName.split('.'); + return { serverName, toolName: rest.join('.') }; + }), + validateToolArgs: jest.fn().mockReturnValue({ valid: true }), + MCPToolStateStore: jest.fn().mockImplementation(() => ({ + // initialize config from client tools is invoked during MCPClient.initialize + // provide a no-op mock so tests that don't assert this behavior don't fail + initConfigFromClientTools: jest.fn(), + })), + })); + + // Ensure initialize can construct a client with getTools/close methods + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: jest.fn().mockImplementation(() => ({ + getTools: jest.fn().mockResolvedValue([]), + close: jest.fn().mockResolvedValue(undefined), + })), + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath) as any; + + await client.initialize(); + + const invoke = jest.fn().mockResolvedValue({ ok: true }); + client.clientTools = [{ name: 'serverA.tool1', schema: {}, invoke }]; + client.mcpToolState = { + isToolEnabled: jest.fn().mockReturnValue(true), + recordToolUsage: jest.fn(), + }; + client.isInitialized = true; + client.client = {}; + + const res = await client.mcpExecuteTool('serverA.tool1', [{ a: 1 }], 'call-1'); + + expect(res.success).toBe(true); + expect(res.result).toEqual({ ok: true }); + expect(res.toolCallId).toBe('call-1'); + expect(client.mcpToolState.recordToolUsage).toHaveBeenCalledWith('serverA', 'tool1'); + }); + + it('returns error when parameter validation fails', async () => { + jest.resetModules(); + jest.doMock('./MCPToolStateStore', () => ({ + parseServerNameToolName: jest + .fn() + .mockReturnValue({ serverName: 'serverA', toolName: 'tool1' }), + validateToolArgs: jest.fn().mockReturnValue({ valid: false, error: 'bad-params' }), + MCPToolStateStore: jest.fn().mockImplementation(() => ({ + initConfigFromClientTools: jest.fn(), + })), + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath) as any; + + // ensure the client is initialized so mcpExecuteTool follows the normal execution path + await client.initialize(); + + client.clientTools = [{ name: 'serverA.tool1', schema: {}, invoke: jest.fn() }]; + client.mcpToolState = { + isToolEnabled: jest.fn().mockReturnValue(true), + recordToolUsage: jest.fn(), + }; + client.isInitialized = true; + // provide a minimal client object so mcpExecuteTool does not early-return + client.client = {}; + + const res = await client.mcpExecuteTool('serverA.tool1', [], 'call-2'); + expect(res.success).toBe(false); + expect(res.error).toMatch(/Parameter validation failed: bad-params/); + expect(res.toolCallId).toBe('call-2'); + }); + + it('returns error when tool is disabled', async () => { + jest.resetModules(); + jest.doMock('./MCPToolStateStore', () => ({ + parseServerNameToolName: jest.fn().mockReturnValue({ serverName: 's', toolName: 't' }), + validateToolArgs: jest.fn().mockReturnValue({ valid: true }), + MCPToolStateStore: jest.fn().mockImplementation(() => ({})), + })); + + const client = new MCPClient(cfgPath, settingsPath) as any; + + client.clientTools = [{ name: 's.t', schema: {}, invoke: jest.fn() }]; + client.mcpToolState = { + isToolEnabled: jest.fn().mockReturnValue(false), + recordToolUsage: jest.fn(), + }; + client.isInitialized = true; + client.client = {}; + + const res = await client.mcpExecuteTool('s.t', [], 'call-3'); + expect(res.success).toBe(false); + expect(res.error).toMatch(/disabled/); + expect(res.toolCallId).toBe('call-3'); + }); + + it('returns error when tool not found', async () => { + jest.resetModules(); + jest.doMock('./MCPToolStateStore', () => ({ + parseServerNameToolName: jest + .fn() + .mockReturnValue({ serverName: 'srv', toolName: 'missing' }), + validateToolArgs: jest.fn().mockReturnValue({ valid: true }), + MCPToolStateStore: jest.fn().mockImplementation(() => ({})), + })); + + const client = new MCPClient(cfgPath, settingsPath) as any; + + // clientTools does not contain the requested tool + client.clientTools = [{ name: 'srv.other', schema: {}, invoke: jest.fn() }]; + client.mcpToolState = { + isToolEnabled: jest.fn().mockReturnValue(true), + recordToolUsage: jest.fn(), + }; + client.isInitialized = true; + // provide a minimal client object so mcpExecuteTool does not early-return + client.client = {}; + + const res = await client.mcpExecuteTool('srv.missing', [], 'call-4'); + expect(res.success).toBe(false); + expect(res.error).toMatch(/not found/); + expect(res.toolCallId).toBe('call-4'); + }); + + it('returns undefined when mcpToolState is not set', async () => { + jest.resetModules(); + // Keep default behavior for parse/validate but it's irrelevant here + jest.doMock('./MCPToolStateStore', () => ({ + parseServerNameToolName: jest.fn().mockReturnValue({ serverName: 'x', toolName: 'y' }), + validateToolArgs: jest.fn().mockReturnValue({ valid: true }), + MCPToolStateStore: jest.fn().mockImplementation(() => ({})), + })); + + const client = new MCPClient(cfgPath, settingsPath) as any; + + client.clientTools = [{ name: 'x.y', schema: {}, invoke: jest.fn() }]; + client.mcpToolState = null; + client.isInitialized = true; + + const res = await client.mcpExecuteTool('x.y', [], 'call-5'); + expect(res).toBeUndefined(); + }); +}); diff --git a/app/electron/mcp/MCPClient.ts b/app/electron/mcp/MCPClient.ts index 40730214cc0..be37dfa1d59 100644 --- a/app/electron/mcp/MCPClient.ts +++ b/app/electron/mcp/MCPClient.ts @@ -18,7 +18,7 @@ import type { DynamicStructuredTool } from '@langchain/core/dist/tools/index'; import { MultiServerMCPClient } from '@langchain/mcp-adapters'; import { type BrowserWindow, dialog } from 'electron'; import { hasClusterDependentServers, makeMcpServersFromSettings } from './MCPSettings'; -import { MCPToolStateStore } from './MCPToolStateStore'; +import { MCPToolStateStore, parseServerNameToolName, validateToolArgs } from './MCPToolStateStore'; const DEBUG = true; @@ -248,6 +248,64 @@ export default class MCPClient { throw error; } } + + /** + * Execute an MCP tool with given parameters. + * + * @param toolName - The full name of the tool to execute (including server prefix) + * @param args - The arguments to pass to the tool + * @param toolCallId - Unique identifier for this tool call + * + * @returns Result object containing success status and output or error message + */ + private async mcpExecuteTool(toolName: string, args: any[], toolCallId: string) { + console.log('args in mcp-execute-tool:', args); + if (!this.mcpToolState) { + return; + } + try { + await this.initializeClient(); + if (!this.client || this.clientTools.length === 0) { + throw new Error('MCP client not initialized or no tools available'); + } + // Parse tool name + const { serverName, toolName: actualToolName } = parseServerNameToolName(toolName); + + // Check if tool is enabled + const isEnabled = this.mcpToolState.isToolEnabled(serverName, actualToolName); + if (!isEnabled) { + throw new Error(`Tool ${actualToolName} from server ${serverName} is disabled`); + } + // Find the tool by name + const tool = this.clientTools.find(t => t.name === toolName); + if (!tool) { + throw new Error(`Tool ${toolName} not found`); + } + // Validate parameters against schema from configuration + const validation = validateToolArgs(tool.schema, args); + + if (!validation.valid) { + throw new Error(`Parameter validation failed: ${validation.error}`); + } + console.log(`Executing MCP tool: ${toolName} with args:`, args); + // Execute the tool directly using LangChain's invoke method + const result = await tool.invoke(args); + console.log(`MCP tool ${toolName} executed successfully`); + // Record tool usage + this.mcpToolState.recordToolUsage(serverName, actualToolName); + return { + success: true, + result, + toolCallId, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + toolCallId, + }; + } + } } /** From 052a94742ac5e8c2bcc2e5975aa6f71e01a194a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 6 Nov 2025 23:04:25 -0300 Subject: [PATCH 19/20] app: MCPClient: MCPSettings: Add setupIpcHandlers and ipc methods Added these ipc methods: mcp-execute-tool mcp-get-status mcp-reset-client mcp-update-config mcp-get-config mcp-get-tools-config mcp-update-tools-config mcp-set-tool-enabled mcp-get-tool-stats mcp-cluster-change These will be used via Electron ipc from the frontend/. Co-Authored-By: Ashu Ghildiyal --- app/electron/mcp/MCPClient.ts | 264 +++++++++++++++++++++++++++++++- app/electron/mcp/MCPSettings.ts | 2 +- 2 files changed, 262 insertions(+), 4 deletions(-) diff --git a/app/electron/mcp/MCPClient.ts b/app/electron/mcp/MCPClient.ts index be37dfa1d59..ad1d80ede5d 100644 --- a/app/electron/mcp/MCPClient.ts +++ b/app/electron/mcp/MCPClient.ts @@ -16,9 +16,22 @@ import type { DynamicStructuredTool } from '@langchain/core/dist/tools/index'; import { MultiServerMCPClient } from '@langchain/mcp-adapters'; -import { type BrowserWindow, dialog } from 'electron'; -import { hasClusterDependentServers, makeMcpServersFromSettings } from './MCPSettings'; -import { MCPToolStateStore, parseServerNameToolName, validateToolArgs } from './MCPToolStateStore'; +import { type BrowserWindow, dialog, ipcMain } from 'electron'; +import { + hasClusterDependentServers, + loadMCPSettings, + makeMcpServersFromSettings, + MCPSettings, + saveMCPSettings, + showSettingsChangeDialog, +} from './MCPSettings'; +import { + MCPToolsConfig, + MCPToolStateStore, + parseServerNameToolName, + showToolsConfigConfirmationDialog, + validateToolArgs, +} from './MCPToolStateStore'; const DEBUG = true; @@ -64,6 +77,7 @@ export default class MCPClient { constructor(configPath: string, settingsPath: string) { this.configPath = configPath; this.settingsPath = settingsPath; + this.setupIpcHandlers(); } /** @@ -306,6 +320,250 @@ export default class MCPClient { }; } } + + private async mcpGetStatus() { + return { + isInitialized: this.isInitialized, + hasClient: this.client !== null, + }; + } + + private async mcpResetClient() { + try { + if (!this.mainWindow) { + throw new Error('Main window not set for MCP client'); + } + // Show confirmation dialog + const userConfirmed = await showConfirmationDialog( + this.mainWindow, + 'MCP Client Reset', + 'The application wants to reset the MCP client. This will restart all MCP server connections.', + 'Reset MCP client' + ); + + if (!userConfirmed) { + return { + success: false, + error: 'User cancelled the operation', + }; + } + + console.log('Resetting MCP client...'); + + if (this.client) { + // If the client has a close/dispose method, call it + if (typeof (this.client as any).close === 'function') { + await (this.client as any).close(); + } + } + + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + + // Re-initialize + await this.initializeClient(); + + return { success: true }; + } catch (error) { + console.error('Error resetting MCP client:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + private async mcpUpdateConfig(mcpSettings: MCPSettings) { + try { + if (!this.mainWindow) { + throw new Error('Main window not set for MCP client'); + } + // Get current configuration for comparison + const currentSettings = loadMCPSettings(this.settingsPath); + console.log('Requested MCP configuration update:', mcpSettings); + // Show detailed confirmation dialog with changes + const userConfirmed = await showSettingsChangeDialog( + this.mainWindow, + currentSettings, + mcpSettings + ); + + if (!userConfirmed) { + return { + success: false, + error: 'User cancelled the configuration update', + }; + } + + console.log('Updating MCP configuration with user confirmation...'); + saveMCPSettings(this.settingsPath, mcpSettings); + + // Reset and reinitialize client with new config + if (this.client && typeof this.client.close === 'function') { + await this.client.close(); + } + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + + // Re-initialize with new config + await this.initializeClient(); + + console.log('MCP configuration updated successfully'); + return { success: true }; + } catch (error) { + console.error('Error updating MCP configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + private async mcpGetConfig() { + try { + const currentSettings = loadMCPSettings(this.settingsPath); + + return { + success: true, + config: currentSettings || { enabled: false, servers: [] }, + }; + } catch (error) { + console.error('Error getting MCP configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + config: { enabled: false, servers: [] }, + }; + } + } + + private async mcpGetToolsConfig() { + try { + const toolsConfig = this.mcpToolState?.getConfig(); + return { + success: true, + config: toolsConfig, + }; + } catch (error) { + console.error('Error getting MCP tools configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + config: {}, + }; + } + } + + private async mcpUpdateToolsConfig(toolsConfig: MCPToolsConfig) { + console.log('Requested MCP tools configuration update:', toolsConfig); + try { + if (!this.mainWindow) { + throw new Error('Main window not set for MCP client'); + } + // Show confirmation dialog with detailed changes + const currentToolsConfig = this.mcpToolState?.getConfig() || {}; + const userConfirmed = await showToolsConfigConfirmationDialog( + this.mainWindow, + currentToolsConfig, + toolsConfig + ); + + if (!userConfirmed) { + return { + success: false, + error: 'User cancelled the operation', + }; + } + + this.mcpToolState?.setConfig(toolsConfig); + return { success: true }; + } catch (error) { + console.error('Error updating MCP tools configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + private async mcpSetToolEnabled(serverName: string, toolName: string, enabled: boolean) { + try { + this.mcpToolState?.setToolEnabled(serverName, toolName, enabled); + return { success: true }; + } catch (error) { + console.error('Error setting tool enabled state:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + private async mcpGetToolStats(serverName: string, toolName: string) { + try { + const stats = this.mcpToolState?.getToolStats(serverName, toolName); + return { + success: true, + stats, + }; + } catch (error) { + console.error('Error getting tool statistics:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + stats: null, + }; + } + } + + private async mcpClusterChange(cluster: string | null) { + try { + console.log('Received cluster change event:', cluster); + if (cluster !== null) { + // @todo: support multiple clusters + await this.handleClustersChange([cluster]); + } + return { + success: true, + }; + } catch (error) { + console.error('Error handling cluster change:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + /** + * Setup IPC handlers for MCP operations. + */ + private setupIpcHandlers(): void { + ipcMain?.handle('mcp-execute-tool', async (event, { toolName, args, toolCallId }) => + this.mcpExecuteTool(toolName, args, toolCallId) + ); + ipcMain?.handle('mcp-get-status', async () => this.mcpGetStatus()); + ipcMain?.handle('mcp-reset-client', async () => this.mcpResetClient()); + ipcMain?.handle('mcp-update-config', async (event, mcpSettings: MCPSettings) => + this.mcpUpdateConfig(mcpSettings) + ); + ipcMain?.handle('mcp-get-config', async () => this.mcpGetConfig()); + ipcMain?.handle('mcp-get-tools-config', async () => this.mcpGetToolsConfig()); + ipcMain?.handle('mcp-update-tools-config', async (event, toolsConfig: MCPToolsConfig) => + this.mcpUpdateToolsConfig(toolsConfig) + ); + ipcMain?.handle('mcp-set-tool-enabled', async (event, { serverName, toolName, enabled }) => + this.mcpSetToolEnabled(serverName, toolName, enabled) + ); + ipcMain?.handle('mcp-get-tool-stats', async (event, { serverName, toolName }) => + this.mcpGetToolStats(serverName, toolName) + ); + ipcMain?.handle('mcp-cluster-change', async (event, { cluster }) => + this.mcpClusterChange(cluster) + ); + } } /** diff --git a/app/electron/mcp/MCPSettings.ts b/app/electron/mcp/MCPSettings.ts index 772ed94002d..a6113ed71c5 100644 --- a/app/electron/mcp/MCPSettings.ts +++ b/app/electron/mcp/MCPSettings.ts @@ -22,7 +22,7 @@ import { loadSettings, saveSettings } from '../settings'; const DEBUG = true; -interface MCPSettings { +export interface MCPSettings { /** * Whether MCP is enabled or not */ From c7b7ae4f13be105f19de3b7b91dcc27c4602241c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 6 Nov 2025 23:16:12 -0300 Subject: [PATCH 20/20] app: preload: Add mcp APIs to be exposed to frontend/ Co-Authored-By: Ashu Ghildiyal --- app/electron/preload.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/electron/preload.ts b/app/electron/preload.ts index 4a32eb276a7..ee661feb18c 100644 --- a/app/electron/preload.ts +++ b/app/electron/preload.ts @@ -31,6 +31,7 @@ contextBridge.exposeInMainWorld('desktopApi', { 'request-backend-token', 'request-plugin-permission-secrets', 'request-backend-port', + 'cluster-changed', ]; if (validChannels.includes(channel)) { ipcRenderer.send(channel, data); @@ -60,4 +61,29 @@ contextBridge.exposeInMainWorld('desktopApi', { removeListener: (channel: string, func: (...args: unknown[]) => void) => { ipcRenderer.removeListener(channel, func); }, + + // @todo: move these to the send receive pattern above, restricted to ai-assistant only. + // @todo: do not enable if environment variable disabling mcp is set. + // MCP client APIs + mcp: { + executeTool: (toolName: string, args: Record, toolCallId?: string) => + ipcRenderer.invoke('mcp-execute-tool', { toolName, args, toolCallId }), + getStatus: () => ipcRenderer.invoke('mcp-get-status'), + resetClient: () => ipcRenderer.invoke('mcp-reset-client'), + getConfig: () => ipcRenderer.invoke('mcp-get-config'), + updateConfig: (config: any) => ipcRenderer.invoke('mcp-update-config', config), + getToolsConfig: () => ipcRenderer.invoke('mcp-get-tools-config'), + updateToolsConfig: (config: any) => ipcRenderer.invoke('mcp-update-tools-config', config), + setToolEnabled: (serverName: string, toolName: string, enabled: boolean) => + ipcRenderer.invoke('mcp-set-tool-enabled', { serverName, toolName, enabled }), + getToolStats: (serverName: string, toolName: string) => + ipcRenderer.invoke('mcp-get-tool-stats', { serverName, toolName }), + clusterChange: (cluster: string | null) => + ipcRenderer.invoke('mcp-cluster-change', { cluster }), + }, + + // Notify cluster change (for MCP server restart) + notifyClusterChange: (cluster: string | null) => { + ipcRenderer.send('cluster-changed', cluster); + }, });