From 557a186508b55a7ee1620404ccbf370461fc542d Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Thu, 17 Sep 2026 19:12:29 +0530 Subject: [PATCH 01/13] fix(security): bump sharp to 0.35.4 and require Node >=22 sharp <=0.35.4-rc.0 inherits high-severity libvips/libheif advisories (CVE-2026-33327/33328/35590/35591 and GHSA-g89c-p67h-r497 / GHSA-2jg2-4ch7-h545). The fix ships in sharp 0.35.4, which is a semver major and requires Node >=20.9.0. - Bump sharp ^0.34.5 -> ^0.35.4 (refreshes the bundled libvips/libheif). - Raise engines.node >=18 -> >=22 to match what CI, the Docker image, and the recommended setup already run, and to satisfy sharp's floor. - README: update the "Node version >= 18.0" notes to >= 22.0. No application code changes: the only sharp call site (src/lib/utils.ts `sharp(buffer).png({ quality }).toBuffer()`) is unchanged across 0.34 -> 0.35. Verified on Node 22: npm audit clears the sharp advisory; lint, typecheck, and the full test suite pass; and an end-to-end image smoke test (a >1MB PNG through maybeCompressBase64) compresses correctly with the new libvips. BREAKING: drops support for Node 18/19 for consumers of the npm package. Co-Authored-By: Claude Opus 4.8 --- README.md | 6 +- package-lock.json | 314 ++++++++++++++++++++++++++-------------------- package.json | 4 +- 3 files changed, 182 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 8bf6d079..fb2f392c 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Click on the buttons below to install MCP in your respective IDE: > **Note:** The hosted Remote MCP server (`mcp.browserstack.com`) is now fully stateless over Streamable HTTP — any Streamable-HTTP client (Claude, Cursor, VS Code, ChatGPT) connects with no configuration change. -#### Note : Ensure you are using Node version >= `18.0` +#### Note : Ensure you are using Node version >= `22.0` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) - To Upgrade Node : - 1. On macOS `(Homebrew) - brew update && brew upgrade node or if using (nvm) - nvm install 22.15.0 && nvm use 22.15.0 && nvm alias default 22.15.0` @@ -158,7 +158,7 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and ## 🛠️ Installation ### 📋 Prerequisites for MCP Setup -#### Note : Ensure you are using Node version >= `18.0` +#### Note : Ensure you are using Node version >= `22.0` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) ### **One Click MCP Setup** @@ -178,7 +178,7 @@ Click on the buttons below to install MCP in your respective IDE: - Once you have an account (and purchased appropriate plan), note down your `username` and `access_key` from [Account Settings](https://www.browserstack.com/accounts/profile/details). -2. #### Note : Ensure you are using Node version >= `18.0` +2. #### Note : Ensure you are using Node version >= `22.0` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) diff --git a/package-lock.json b/package-lock.json index 304cb2c8..8e6833ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "form-data": "^4.0.5", "pino": "^10.3.1", "pino-pretty": "^13.1.3", - "sharp": "^0.34.5", + "sharp": "^0.35.4", "uuid": "^13.0.0", "webdriverio": "^9.27.0", "zod": "^4.3.6" @@ -40,13 +40,13 @@ "vitest": "^4.0.16" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", "optional": true, "dependencies": { @@ -725,9 +725,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", "cpu": [ "arm64" ], @@ -737,19 +737,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", "cpu": [ "x64" ], @@ -759,19 +759,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", "cpu": [ "arm64" ], @@ -785,9 +804,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", "cpu": [ "x64" ], @@ -801,9 +820,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", "cpu": [ "arm" ], @@ -817,9 +836,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", "cpu": [ "arm64" ], @@ -833,9 +852,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", "cpu": [ "ppc64" ], @@ -849,9 +868,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", "cpu": [ "riscv64" ], @@ -865,9 +884,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", "cpu": [ "s390x" ], @@ -881,9 +900,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", "cpu": [ "x64" ], @@ -897,9 +916,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", "cpu": [ "arm64" ], @@ -913,9 +932,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", "cpu": [ "x64" ], @@ -929,9 +948,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", "cpu": [ "arm" ], @@ -941,19 +960,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", "cpu": [ "arm64" ], @@ -963,19 +982,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.3" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", "cpu": [ "ppc64" ], @@ -985,19 +1004,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.3" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", "cpu": [ "riscv64" ], @@ -1007,19 +1026,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", "cpu": [ "s390x" ], @@ -1029,19 +1048,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", "cpu": [ "x64" ], @@ -1051,19 +1070,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", "cpu": [ "arm64" ], @@ -1073,19 +1092,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", "cpu": [ "x64" ], @@ -1095,38 +1114,54 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", "cpu": [ "wasm32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.4" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", "cpu": [ "arm64" ], @@ -1136,16 +1171,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", "cpu": [ "ia32" ], @@ -1155,16 +1190,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", "cpu": [ "x64" ], @@ -1174,7 +1209,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -6317,9 +6352,9 @@ "license": "BSD-3-Clause" }, "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6413,47 +6448,52 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shebang-command": { diff --git a/package.json b/package.json index fb822c76..53663fad 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "automation" ], "engines": { - "node": ">=18" + "node": ">=22" }, "author": "", "license": "ISC", @@ -44,7 +44,7 @@ "form-data": "^4.0.5", "pino": "^10.3.1", "pino-pretty": "^13.1.3", - "sharp": "^0.34.5", + "sharp": "^0.35.4", "uuid": "^13.0.0", "webdriverio": "^9.27.0", "zod": "^4.3.6" From db8850057fa076c605fde9e945f1d0651839f6a7 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Tue, 22 Sep 2026 19:49:19 +0530 Subject: [PATCH 02/13] fix(security): lazy-load sharp + advisory Node nudge (bridge to Node 22) sharp 0.35.x's native binding fails to load on Node < 20.9. Since sharp was a top-level import pulled in at boot via the tool chain, a straight bump crashed the whole server on startup for those runtimes (~1% of users), not just the screenshot tools. - Load sharp lazily inside maybeCompressBase64 (only for images > 1 MB), wrapped in try/catch: on Node < 20.9 the server boots and every tool works; oversized screenshots simply return uncompressed. - Replace the buggy string-compare Node guard with a numeric check that warns (does not throw) on Node < 20.9, nudging an upgrade to Node 22. - Soften engines to >=20.9.0 (sharp's real floor) for this bridge; a follow-up will drop the lazy-load and require Node >= 22 once telemetry shows the <20.9 tail has drained. Co-Authored-By: Claude Opus 4.8 --- package.json | 2 +- src/lib/utils.ts | 19 +++++++++++++++---- src/oninitialized.ts | 17 ++++++++++++----- tests/lib/utils.test.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 tests/lib/utils.test.ts diff --git a/package.json b/package.json index 53663fad..d3d03276 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "automation" ], "engines": { - "node": ">=22" + "node": ">=20.9.0" }, "author": "", "license": "ISC", diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5b8d5af6..0429bfd6 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,10 +1,10 @@ -import sharp from "sharp"; import type { ApiResponse } from "./apiClient.js"; import { BrowserStackConfig } from "./types.js"; import { getBrowserStackAuth } from "./get-auth.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { trackMCP } from "../index.js"; +import logger from "../logger.js"; export function sanitizeUrlParam(param: string): string { // Remove any characters that could be used for command injection @@ -25,9 +25,20 @@ export async function maybeCompressBase64(base64: string): Promise { const estimatedQuality = Math.floor(sizeRatio * 100); const quality = Math.min(95, Math.max(30, estimatedQuality)); - const compressedBuffer = await sharp(buffer).png({ quality }).toBuffer(); - - return compressedBuffer.toString("base64"); + // sharp is loaded lazily (and only for oversized images) so the server still + // boots on Node < 20.9, where sharp 0.35.x's native binding fails to load. + // On such runtimes we degrade gracefully: return the uncompressed image. + try { + const { default: sharp } = await import("sharp"); + const compressedBuffer = await sharp(buffer).png({ quality }).toBuffer(); + return compressedBuffer.toString("base64"); + } catch (err) { + logger.warn( + "Image compression unavailable (sharp failed to load — e.g. Node < 20.9); returning the uncompressed image. %s", + err instanceof Error ? err.message : String(err), + ); + return base64; + } } export async function assertOkResponse( diff --git a/src/oninitialized.ts b/src/oninitialized.ts index 1aa04205..d053dbc2 100644 --- a/src/oninitialized.ts +++ b/src/oninitialized.ts @@ -1,14 +1,21 @@ import { trackMCP } from "./lib/instrumentation.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { shouldSendStartedEvent } from "./lib/device-cache.js"; +import logger from "./logger.js"; export function setupOnInitialized(server: McpServer, config?: any) { - const nodeVersion = process.versions.node; + // Numeric Node.js version check (string compare mishandles e.g. "9" vs "18"). + const [major = 0, minor = 0] = process.versions.node.split(".").map(Number); + const isBelow20_9 = major < 20 || (major === 20 && minor < 9); - // Check for Node.js version - if (nodeVersion < "18.0.0") { - throw new Error( - "Node version is not supported. Please upgrade to 18.0.0 or later.", + // Advisory nudge only — the server still runs on older Node (image + // compression degrades gracefully), and a future release will require + // Node >= 22. We don't throw so existing users are never hard-broken. + if (isBelow20_9) { + logger.warn( + `Node ${process.versions.node} detected. Image compression is disabled on ` + + `Node < 20.9, and an upcoming release will require Node >= 22. ` + + `Please upgrade (Node 22 LTS recommended).`, ); } diff --git a/tests/lib/utils.test.ts b/tests/lib/utils.test.ts new file mode 100644 index 00000000..de320e56 --- /dev/null +++ b/tests/lib/utils.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, vi, beforeEach, Mock } from "vitest"; +import sharp from "sharp"; +import logger from "../../src/logger"; +import { maybeCompressBase64 } from "../../src/lib/utils"; + +// utils.ts imports trackMCP from the entry module; stub it so importing the +// real utils under test does not pull in the server bootstrap. +vi.mock("../../src/index", () => ({ trackMCP: vi.fn() })); +vi.mock("../../src/logger", () => ({ + default: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }, +})); + +// sharp itself is globally mocked in tests/setup.ts (native module). +const ONE_MB = 1048576; +const overSized = () => Buffer.alloc(ONE_MB + 1, 7).toString("base64"); + +describe("maybeCompressBase64", () => { + beforeEach(() => vi.clearAllMocks()); + + it("returns the input unchanged when already <= 1 MB (never touches sharp)", async () => { + const small = Buffer.from("small-image").toString("base64"); + expect(await maybeCompressBase64(small)).toBe(small); + expect(sharp as unknown as Mock).not.toHaveBeenCalled(); + }); + + it("lazily loads sharp and returns the compressed image when > 1 MB", async () => { + const out = await maybeCompressBase64(overSized()); + // setup.ts mock returns Buffer.from("mock-image") + expect(out).toBe(Buffer.from("mock-image").toString("base64")); + expect(sharp as unknown as Mock).toHaveBeenCalledTimes(1); + }); + + it("falls back to the uncompressed image (and warns) when sharp fails to load", async () => { + (sharp as unknown as Mock).mockImplementationOnce(() => { + throw new Error("Could not load the sharp module (Node < 20.9)"); + }); + const input = overSized(); + expect(await maybeCompressBase64(input)).toBe(input); // unchanged fallback + expect(logger.warn as Mock).toHaveBeenCalledTimes(1); + }); +}); From 4c3e77ad71fe7e64cfef9639c35db066150a69ad Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Tue, 22 Sep 2026 20:03:07 +0530 Subject: [PATCH 03/13] test: cover Node-version nudge boundary in setupOnInitialized Co-Authored-By: Claude Opus 4.8 --- tests/oninitialized.test.ts | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/oninitialized.test.ts diff --git a/tests/oninitialized.test.ts b/tests/oninitialized.test.ts new file mode 100644 index 00000000..0ff758cb --- /dev/null +++ b/tests/oninitialized.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, vi, beforeEach, afterEach, Mock } from "vitest"; +import logger from "../src/logger"; +import { setupOnInitialized } from "../src/oninitialized"; + +vi.mock("../src/logger", () => ({ + default: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }, +})); +vi.mock("../src/lib/instrumentation", () => ({ trackMCP: vi.fn() })); +vi.mock("../src/lib/device-cache", () => ({ shouldSendStartedEvent: () => false })); + +// Minimal McpServer stub — setupOnInitialized only sets server.server.oninitialized. +const makeServer = () => + ({ server: { oninitialized: undefined, getClientVersion: () => ({}) } }) as any; + +// process.versions.node is read-only; override it per-case then restore. +const realNode = process.versions.node; +const setNode = (v: string) => + Object.defineProperty(process.versions, "node", { value: v, configurable: true }); + +describe("setupOnInitialized – Node version nudge", () => { + beforeEach(() => vi.clearAllMocks()); + afterEach(() => setNode(realNode)); + + it.each(["18.19.0", "20.0.0", "20.8.9", "16.20.0"])( + "warns on Node < 20.9 (%s) without throwing", + (v) => { + setNode(v); + expect(() => setupOnInitialized(makeServer())).not.toThrow(); + expect(logger.warn as Mock).toHaveBeenCalledTimes(1); + }, + ); + + it.each(["20.9.0", "20.10.0", "22.11.0", "24.3.0"])( + "does not warn on Node >= 20.9 (%s)", + (v) => { + setNode(v); + setupOnInitialized(makeServer()); + expect(logger.warn as Mock).not.toHaveBeenCalled(); + }, + ); +}); From fabc8c7877e812573b6b428a805a1bb89214fd68 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Tue, 22 Sep 2026 20:50:17 +0530 Subject: [PATCH 04/13] feat(security): surface Node upgrade nudge on every tool response ( 21.x.x): "Please use Node version > 21.x.x (Node 22 LTS recommended)". No-op on Node >= 22, so the ~89% already on 22+ see nothing. Co-Authored-By: Claude Opus 4.8 --- src/lib/node-version-notice.ts | 32 ++++++++++++++++++++ src/server-factory.ts | 31 +++++++++++++++++++ tests/lib/node-version-notice.test.ts | 43 +++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 src/lib/node-version-notice.ts create mode 100644 tests/lib/node-version-notice.test.ts diff --git a/src/lib/node-version-notice.ts b/src/lib/node-version-notice.ts new file mode 100644 index 00000000..899279ed --- /dev/null +++ b/src/lib/node-version-notice.ts @@ -0,0 +1,32 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +/** + * Returns a user-facing upgrade nudge for runtimes below Node 22 (> 21.x.x), + * or "" on supported runtimes. sharp 0.35.x needs Node >= 20.9 (image + * compression is disabled below that via graceful fallback), and an upcoming + * release will require Node >= 22 — so we steer users to Node 22 LTS. + */ +export function nodeUpgradeNotice(version: string = process.versions.node): string { + const major = Number(version.split(".")[0]) || 0; + if (major >= 22) return ""; + return ( + `⚠️ Please use Node version > 21.x.x (Node 22 LTS recommended). ` + + `This server is running on Node ${version}; older versions are unsupported ` + + `and image compression is disabled below Node 20.9.` + ); +} + +/** + * Prepends the upgrade notice to a tool result's content when one applies. + * A no-op (returns the result unchanged) on Node >= 22 or for malformed results. + */ +export function withNodeUpgradeNotice( + result: T, + notice: string = nodeUpgradeNotice(), +): T { + if (!notice || !result || !Array.isArray(result.content)) return result; + return { + ...result, + content: [{ type: "text", text: notice }, ...result.content], + }; +} diff --git a/src/server-factory.ts b/src/server-factory.ts index c34f688b..ba4116fd 100644 --- a/src/server-factory.ts +++ b/src/server-factory.ts @@ -21,6 +21,10 @@ import { setupOnInitialized } from "./oninitialized.js"; import { BrowserStackConfig } from "./lib/types.js"; import addRCATools from "./tools/rca-agent.js"; import addAskBrowserStackAITool from "./tools/ask-browserstack/register.js"; +import { + nodeUpgradeNotice, + withNodeUpgradeNotice, +} from "./lib/node-version-notice.js"; /** * Wrapper class for BrowserStack MCP Server @@ -42,9 +46,36 @@ export class BrowserStackMcpServer { }); setupOnInitialized(this.server, this.config); + this.applyNodeUpgradeNotice(); this.registerTools(); } + /** + * On runtimes below Node 22, wrap server.tool so every registered tool's + * response carries a user-visible upgrade nudge (a stderr log alone never + * reaches the client chat). No-op on Node >= 22. + */ + private applyNodeUpgradeNotice() { + const notice = nodeUpgradeNotice(); + if (!notice) return; + + const server = this.server; + const originalTool = server.tool.bind(server) as ( + ...args: unknown[] + ) => RegisteredTool; + + (server as unknown as { tool: (...args: unknown[]) => RegisteredTool }).tool = + (...args: unknown[]): RegisteredTool => { + const lastIndex = args.length - 1; + const cb = args[lastIndex]; + if (typeof cb === "function") { + args[lastIndex] = async (...cbArgs: unknown[]) => + withNodeUpgradeNotice(await cb(...cbArgs), notice); + } + return originalTool(...args); + }; + } + /** * Calls each tool-adder function and collects their returned tools */ diff --git a/tests/lib/node-version-notice.test.ts b/tests/lib/node-version-notice.test.ts new file mode 100644 index 00000000..5e2c5548 --- /dev/null +++ b/tests/lib/node-version-notice.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from "vitest"; +import { + nodeUpgradeNotice, + withNodeUpgradeNotice, +} from "../../src/lib/node-version-notice"; + +describe("nodeUpgradeNotice", () => { + it.each(["18.19.0", "20.9.0", "21.7.3"])( + "returns a nudge for Node < 22 (%s)", + (v) => { + expect(nodeUpgradeNotice(v)).toContain("Node version > 21.x.x"); + expect(nodeUpgradeNotice(v)).toContain(v); + }, + ); + + it.each(["22.0.0", "24.3.1", "26.1.0"])( + "returns empty on Node >= 22 (%s)", + (v) => { + expect(nodeUpgradeNotice(v)).toBe(""); + }, + ); +}); + +describe("withNodeUpgradeNotice", () => { + const result = { content: [{ type: "text", text: "original" }] }; + + it("prepends the notice when one applies", () => { + const out = withNodeUpgradeNotice(result, "⚠️ upgrade"); + expect(out.content).toHaveLength(2); + expect(out.content[0]).toEqual({ type: "text", text: "⚠️ upgrade" }); + expect(out.content[1]).toEqual({ type: "text", text: "original" }); + }); + + it("returns the result unchanged when the notice is empty (Node >= 22)", () => { + const out = withNodeUpgradeNotice(result, ""); + expect(out).toBe(result); + }); + + it("is a no-op on malformed results", () => { + const bad = {} as any; + expect(withNodeUpgradeNotice(bad, "⚠️ upgrade")).toBe(bad); + }); +}); From 00f62a614b33370dd9d7f5c1dcdbe8d8f7781234 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Tue, 22 Sep 2026 21:07:58 +0530 Subject: [PATCH 05/13] chore(security): trim Node upgrade notice text Drop the image-compression detail and use future tense per review: 'older versions will be unsupported.' Central middleware unchanged. Co-Authored-By: Claude Opus 4.8 --- src/lib/node-version-notice.ts | 7 ++++--- src/server-factory.ts | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/lib/node-version-notice.ts b/src/lib/node-version-notice.ts index 899279ed..9e24d51f 100644 --- a/src/lib/node-version-notice.ts +++ b/src/lib/node-version-notice.ts @@ -6,13 +6,14 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; * compression is disabled below that via graceful fallback), and an upcoming * release will require Node >= 22 — so we steer users to Node 22 LTS. */ -export function nodeUpgradeNotice(version: string = process.versions.node): string { +export function nodeUpgradeNotice( + version: string = process.versions.node, +): string { const major = Number(version.split(".")[0]) || 0; if (major >= 22) return ""; return ( `⚠️ Please use Node version > 21.x.x (Node 22 LTS recommended). ` + - `This server is running on Node ${version}; older versions are unsupported ` + - `and image compression is disabled below Node 20.9.` + `This server is running on Node ${version}; older versions will be unsupported.` ); } diff --git a/src/server-factory.ts b/src/server-factory.ts index ba4116fd..3c0b3358 100644 --- a/src/server-factory.ts +++ b/src/server-factory.ts @@ -64,16 +64,17 @@ export class BrowserStackMcpServer { ...args: unknown[] ) => RegisteredTool; - (server as unknown as { tool: (...args: unknown[]) => RegisteredTool }).tool = - (...args: unknown[]): RegisteredTool => { - const lastIndex = args.length - 1; - const cb = args[lastIndex]; - if (typeof cb === "function") { - args[lastIndex] = async (...cbArgs: unknown[]) => - withNodeUpgradeNotice(await cb(...cbArgs), notice); - } - return originalTool(...args); - }; + ( + server as unknown as { tool: (...args: unknown[]) => RegisteredTool } + ).tool = (...args: unknown[]): RegisteredTool => { + const lastIndex = args.length - 1; + const cb = args[lastIndex]; + if (typeof cb === "function") { + args[lastIndex] = async (...cbArgs: unknown[]) => + withNodeUpgradeNotice(await cb(...cbArgs), notice); + } + return originalTool(...args); + }; } /** From ff1e93e8a04e64c6c02fd4669c5d37bf9bb6b716 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Tue, 22 Sep 2026 21:14:39 +0530 Subject: [PATCH 06/13] refactor(security): centralize Node threshold, trim comments, engines >=21 - Single RECOMMENDED_NODE_MAJOR constant drives the notice text and check (no scattered version literals). - Startup log reuses nodeUpgradeNotice() instead of a duplicated message. - Remove explanatory comments; engines floor >=21.0.0. Co-Authored-By: Claude Opus 4.8 --- package.json | 2 +- src/lib/node-version-notice.ts | 19 ++++++------------- src/lib/utils.ts | 5 +---- src/oninitialized.ts | 17 ++++------------- src/server-factory.ts | 5 ----- tests/oninitialized.test.ts | 8 ++++---- 6 files changed, 16 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index d3d03276..97f09f4f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "automation" ], "engines": { - "node": ">=20.9.0" + "node": ">=21.0.0" }, "author": "", "license": "ISC", diff --git a/src/lib/node-version-notice.ts b/src/lib/node-version-notice.ts index 9e24d51f..08f56eec 100644 --- a/src/lib/node-version-notice.ts +++ b/src/lib/node-version-notice.ts @@ -1,26 +1,19 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -/** - * Returns a user-facing upgrade nudge for runtimes below Node 22 (> 21.x.x), - * or "" on supported runtimes. sharp 0.35.x needs Node >= 20.9 (image - * compression is disabled below that via graceful fallback), and an upcoming - * release will require Node >= 22 — so we steer users to Node 22 LTS. - */ +const RECOMMENDED_NODE_MAJOR = 22; + export function nodeUpgradeNotice( version: string = process.versions.node, ): string { const major = Number(version.split(".")[0]) || 0; - if (major >= 22) return ""; + if (major >= RECOMMENDED_NODE_MAJOR) return ""; return ( - `⚠️ Please use Node version > 21.x.x (Node 22 LTS recommended). ` + - `This server is running on Node ${version}; older versions will be unsupported.` + `⚠️ Please use Node version > ${RECOMMENDED_NODE_MAJOR - 1}.x.x ` + + `(Node ${RECOMMENDED_NODE_MAJOR} LTS recommended). This server is running on ` + + `Node ${version}; older versions will be unsupported.` ); } -/** - * Prepends the upgrade notice to a tool result's content when one applies. - * A no-op (returns the result unchanged) on Node >= 22 or for malformed results. - */ export function withNodeUpgradeNotice( result: T, notice: string = nodeUpgradeNotice(), diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 0429bfd6..31af99f8 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -25,16 +25,13 @@ export async function maybeCompressBase64(base64: string): Promise { const estimatedQuality = Math.floor(sizeRatio * 100); const quality = Math.min(95, Math.max(30, estimatedQuality)); - // sharp is loaded lazily (and only for oversized images) so the server still - // boots on Node < 20.9, where sharp 0.35.x's native binding fails to load. - // On such runtimes we degrade gracefully: return the uncompressed image. try { const { default: sharp } = await import("sharp"); const compressedBuffer = await sharp(buffer).png({ quality }).toBuffer(); return compressedBuffer.toString("base64"); } catch (err) { logger.warn( - "Image compression unavailable (sharp failed to load — e.g. Node < 20.9); returning the uncompressed image. %s", + "Image compression unavailable (sharp failed to load); returning the uncompressed image. %s", err instanceof Error ? err.message : String(err), ); return base64; diff --git a/src/oninitialized.ts b/src/oninitialized.ts index d053dbc2..8e8617ff 100644 --- a/src/oninitialized.ts +++ b/src/oninitialized.ts @@ -2,21 +2,12 @@ import { trackMCP } from "./lib/instrumentation.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { shouldSendStartedEvent } from "./lib/device-cache.js"; import logger from "./logger.js"; +import { nodeUpgradeNotice } from "./lib/node-version-notice.js"; export function setupOnInitialized(server: McpServer, config?: any) { - // Numeric Node.js version check (string compare mishandles e.g. "9" vs "18"). - const [major = 0, minor = 0] = process.versions.node.split(".").map(Number); - const isBelow20_9 = major < 20 || (major === 20 && minor < 9); - - // Advisory nudge only — the server still runs on older Node (image - // compression degrades gracefully), and a future release will require - // Node >= 22. We don't throw so existing users are never hard-broken. - if (isBelow20_9) { - logger.warn( - `Node ${process.versions.node} detected. Image compression is disabled on ` + - `Node < 20.9, and an upcoming release will require Node >= 22. ` + - `Please upgrade (Node 22 LTS recommended).`, - ); + const notice = nodeUpgradeNotice(); + if (notice) { + logger.warn(notice); } server.server.oninitialized = () => { diff --git a/src/server-factory.ts b/src/server-factory.ts index 3c0b3358..209c5f1d 100644 --- a/src/server-factory.ts +++ b/src/server-factory.ts @@ -50,11 +50,6 @@ export class BrowserStackMcpServer { this.registerTools(); } - /** - * On runtimes below Node 22, wrap server.tool so every registered tool's - * response carries a user-visible upgrade nudge (a stderr log alone never - * reaches the client chat). No-op on Node >= 22. - */ private applyNodeUpgradeNotice() { const notice = nodeUpgradeNotice(); if (!notice) return; diff --git a/tests/oninitialized.test.ts b/tests/oninitialized.test.ts index 0ff758cb..621fd513 100644 --- a/tests/oninitialized.test.ts +++ b/tests/oninitialized.test.ts @@ -21,8 +21,8 @@ describe("setupOnInitialized – Node version nudge", () => { beforeEach(() => vi.clearAllMocks()); afterEach(() => setNode(realNode)); - it.each(["18.19.0", "20.0.0", "20.8.9", "16.20.0"])( - "warns on Node < 20.9 (%s) without throwing", + it.each(["18.19.0", "20.9.0", "21.7.3", "16.20.0"])( + "warns on Node < 22 (%s) without throwing", (v) => { setNode(v); expect(() => setupOnInitialized(makeServer())).not.toThrow(); @@ -30,8 +30,8 @@ describe("setupOnInitialized – Node version nudge", () => { }, ); - it.each(["20.9.0", "20.10.0", "22.11.0", "24.3.0"])( - "does not warn on Node >= 20.9 (%s)", + it.each(["22.0.0", "22.11.0", "24.3.0", "26.1.0"])( + "does not warn on Node >= 22 (%s)", (v) => { setNode(v); setupOnInitialized(makeServer()); From cf171e39fdc91e060561ad6b20d2f1e1eddfc5d6 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Wed, 23 Sep 2026 12:19:31 +0530 Subject: [PATCH 07/13] docs: align README Node minimum with engines (>=21) Co-Authored-By: Claude Opus 4.8 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fb2f392c..b20374fe 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Click on the buttons below to install MCP in your respective IDE: > **Note:** The hosted Remote MCP server (`mcp.browserstack.com`) is now fully stateless over Streamable HTTP — any Streamable-HTTP client (Claude, Cursor, VS Code, ChatGPT) connects with no configuration change. -#### Note : Ensure you are using Node version >= `22.0` +#### Note : Ensure you are using Node version >= `21.0` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) - To Upgrade Node : - 1. On macOS `(Homebrew) - brew update && brew upgrade node or if using (nvm) - nvm install 22.15.0 && nvm use 22.15.0 && nvm alias default 22.15.0` @@ -158,7 +158,7 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and ## 🛠️ Installation ### 📋 Prerequisites for MCP Setup -#### Note : Ensure you are using Node version >= `22.0` +#### Note : Ensure you are using Node version >= `21.0` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) ### **One Click MCP Setup** @@ -178,7 +178,7 @@ Click on the buttons below to install MCP in your respective IDE: - Once you have an account (and purchased appropriate plan), note down your `username` and `access_key` from [Account Settings](https://www.browserstack.com/accounts/profile/details). -2. #### Note : Ensure you are using Node version >= `22.0` +2. #### Note : Ensure you are using Node version >= `21.0` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) From 62e11061a94ab3946af60bc5252a8b16cc8f4d90 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Wed, 23 Sep 2026 12:25:38 +0530 Subject: [PATCH 08/13] chore: use short engines form (>=21) to match repo convention Co-Authored-By: Claude Opus 4.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97f09f4f..2587a7b8 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "automation" ], "engines": { - "node": ">=21.0.0" + "node": ">=21" }, "author": "", "license": "ISC", From 18080bebf88badc7cf9d0facae44b38c75a23d39 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Wed, 23 Sep 2026 13:20:28 +0530 Subject: [PATCH 09/13] fix(security): engines >=20.9.0 (sharp floor), append notice not prepend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review: - engines/README back to >=20.9.0 (sharp's real floor, matches the PR body). >=21 was arbitrary — not the constraint (20.9), not an LTS, not the destination (22) — and EBADENGINE-warned the very sub-21 users the lazy-load bridge exists to support. - Append the upgrade notice as the last content block instead of prepending, so the tool payload stays at content[0] for positional consumers (and matches the intended "append" behavior). Co-Authored-By: Claude Opus 4.8 --- README.md | 6 +++--- package.json | 2 +- src/lib/node-version-notice.ts | 2 +- tests/lib/node-version-notice.test.ts | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b20374fe..39de5c26 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Click on the buttons below to install MCP in your respective IDE: > **Note:** The hosted Remote MCP server (`mcp.browserstack.com`) is now fully stateless over Streamable HTTP — any Streamable-HTTP client (Claude, Cursor, VS Code, ChatGPT) connects with no configuration change. -#### Note : Ensure you are using Node version >= `21.0` +#### Note : Ensure you are using Node version >= `20.9` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) - To Upgrade Node : - 1. On macOS `(Homebrew) - brew update && brew upgrade node or if using (nvm) - nvm install 22.15.0 && nvm use 22.15.0 && nvm alias default 22.15.0` @@ -158,7 +158,7 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and ## 🛠️ Installation ### 📋 Prerequisites for MCP Setup -#### Note : Ensure you are using Node version >= `21.0` +#### Note : Ensure you are using Node version >= `20.9` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) ### **One Click MCP Setup** @@ -178,7 +178,7 @@ Click on the buttons below to install MCP in your respective IDE: - Once you have an account (and purchased appropriate plan), note down your `username` and `access_key` from [Account Settings](https://www.browserstack.com/accounts/profile/details). -2. #### Note : Ensure you are using Node version >= `21.0` +2. #### Note : Ensure you are using Node version >= `20.9` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) diff --git a/package.json b/package.json index 2587a7b8..d3d03276 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "automation" ], "engines": { - "node": ">=21" + "node": ">=20.9.0" }, "author": "", "license": "ISC", diff --git a/src/lib/node-version-notice.ts b/src/lib/node-version-notice.ts index 08f56eec..044d9b88 100644 --- a/src/lib/node-version-notice.ts +++ b/src/lib/node-version-notice.ts @@ -21,6 +21,6 @@ export function withNodeUpgradeNotice( if (!notice || !result || !Array.isArray(result.content)) return result; return { ...result, - content: [{ type: "text", text: notice }, ...result.content], + content: [...result.content, { type: "text", text: notice }], }; } diff --git a/tests/lib/node-version-notice.test.ts b/tests/lib/node-version-notice.test.ts index 5e2c5548..2734860d 100644 --- a/tests/lib/node-version-notice.test.ts +++ b/tests/lib/node-version-notice.test.ts @@ -24,11 +24,11 @@ describe("nodeUpgradeNotice", () => { describe("withNodeUpgradeNotice", () => { const result = { content: [{ type: "text", text: "original" }] }; - it("prepends the notice when one applies", () => { + it("appends the notice when one applies (payload stays at content[0])", () => { const out = withNodeUpgradeNotice(result, "⚠️ upgrade"); expect(out.content).toHaveLength(2); - expect(out.content[0]).toEqual({ type: "text", text: "⚠️ upgrade" }); - expect(out.content[1]).toEqual({ type: "text", text: "original" }); + expect(out.content[0]).toEqual({ type: "text", text: "original" }); + expect(out.content[1]).toEqual({ type: "text", text: "⚠️ upgrade" }); }); it("returns the result unchanged when the notice is empty (Node >= 22)", () => { From 2fa870306d720ecb7830413e59a46307f4f96292 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Wed, 23 Sep 2026 13:40:35 +0530 Subject: [PATCH 10/13] test(e2e): verify Node upgrade notice on a real tools/call round-trip InMemoryTransport client<->server: notice appended (last block) on Node < 22, absent on >= 22. Version-adaptive so it passes on any runtime. Co-Authored-By: Claude Opus 4.8 --- tests/e2e/node-notice.e2e.test.ts | 59 +++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/e2e/node-notice.e2e.test.ts diff --git a/tests/e2e/node-notice.e2e.test.ts b/tests/e2e/node-notice.e2e.test.ts new file mode 100644 index 00000000..56b98fa7 --- /dev/null +++ b/tests/e2e/node-notice.e2e.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, vi } from "vitest"; + +// Keep the tool off the network so we exercise the response path, not the API. +vi.mock("../../src/lib/instrumentation", () => ({ trackMCP: vi.fn() })); +vi.mock("../../src/lib/tm-base-url", () => ({ + getTMBaseURL: vi.fn().mockResolvedValue("https://tm.example.com"), +})); +vi.mock("../../src/lib/apiClient", () => ({ + apiClient: { + get: vi.fn().mockResolvedValue({ data: { success: true, test_cases: [] } }), + post: vi.fn().mockResolvedValue({ data: { success: true } }), + }, +})); + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { BrowserStackMcpServer } from "../../src/server-factory"; + +const NOTICE_MARKER = "Node version > 21.x.x"; + +describe("e2e: Node upgrade notice rides on a real tools/call round-trip", () => { + it("appends the notice below Node 22 and omits it on Node >= 22", async () => { + const major = Number(process.versions.node.split(".")[0]) || 0; + + const config: any = { + "browserstack-username": "u", + "browserstack-access-key": "k", + }; + const bs = new BrowserStackMcpServer(config); + + const [clientT, serverT] = InMemoryTransport.createLinkedPair(); + const client = new Client( + { name: "e2e", version: "1.0.0" }, + { capabilities: {} }, + ); + await Promise.all([ + client.connect(clientT), + bs.getInstance().connect(serverT), + ]); + + const res: any = await client.callTool({ + name: "listTestCases", + arguments: { project_identifier: "PR-1" }, + }); + + const blocks: any[] = res.content ?? []; + const joined = blocks.map((c) => c.text ?? "").join("\n"); + + if (major < 22) { + expect(joined).toContain(NOTICE_MARKER); + // payload stays first; the notice is appended last + expect(blocks[blocks.length - 1].text).toContain(NOTICE_MARKER); + } else { + expect(joined).not.toContain(NOTICE_MARKER); + } + + await client.close(); + }); +}); From b2be129cca4dd4dbc2a8fa9ede975752ff259b2e Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Wed, 23 Sep 2026 14:28:26 +0530 Subject: [PATCH 11/13] test(e2e): cover both notice paths deterministically via faked node version Override process.versions.node before constructing the server so a single run exercises the wrapped (<22) and unchanged (>=22) registration paths, regardless of host Node. Addresses CodeRabbit review. Co-Authored-By: Claude Opus 4.8 --- tests/e2e/node-notice.e2e.test.ts | 84 ++++++++++++++++++------------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/tests/e2e/node-notice.e2e.test.ts b/tests/e2e/node-notice.e2e.test.ts index 56b98fa7..fffdd564 100644 --- a/tests/e2e/node-notice.e2e.test.ts +++ b/tests/e2e/node-notice.e2e.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi, afterEach } from "vitest"; // Keep the tool off the network so we exercise the response path, not the API. vi.mock("../../src/lib/instrumentation", () => ({ trackMCP: vi.fn() })); @@ -18,42 +18,54 @@ import { BrowserStackMcpServer } from "../../src/server-factory"; const NOTICE_MARKER = "Node version > 21.x.x"; -describe("e2e: Node upgrade notice rides on a real tools/call round-trip", () => { - it("appends the notice below Node 22 and omits it on Node >= 22", async () => { - const major = Number(process.versions.node.split(".")[0]) || 0; - - const config: any = { - "browserstack-username": "u", - "browserstack-access-key": "k", - }; - const bs = new BrowserStackMcpServer(config); - - const [clientT, serverT] = InMemoryTransport.createLinkedPair(); - const client = new Client( - { name: "e2e", version: "1.0.0" }, - { capabilities: {} }, - ); - await Promise.all([ - client.connect(clientT), - bs.getInstance().connect(serverT), - ]); - - const res: any = await client.callTool({ - name: "listTestCases", - arguments: { project_identifier: "PR-1" }, - }); - - const blocks: any[] = res.content ?? []; - const joined = blocks.map((c) => c.text ?? "").join("\n"); +// process.versions.node is read-only; override per-case then restore so both +// the wrapped (<22) and unchanged (>=22) registration paths run deterministically +// regardless of the host Node version. +const realNode = process.versions.node; +const setNode = (v: string) => + Object.defineProperty(process.versions, "node", { + value: v, + configurable: true, + }); + +async function callListTestCases(): Promise> { + const config: any = { + "browserstack-username": "u", + "browserstack-access-key": "k", + }; + const bs = new BrowserStackMcpServer(config); // applies wrapper based on faked node + const [clientT, serverT] = InMemoryTransport.createLinkedPair(); + const client = new Client( + { name: "e2e", version: "1.0.0" }, + { capabilities: {} }, + ); + await Promise.all([ + client.connect(clientT), + bs.getInstance().connect(serverT), + ]); + const res: any = await client.callTool({ + name: "listTestCases", + arguments: { project_identifier: "PR-1" }, + }); + await client.close(); + return res.content ?? []; +} - if (major < 22) { - expect(joined).toContain(NOTICE_MARKER); - // payload stays first; the notice is appended last - expect(blocks[blocks.length - 1].text).toContain(NOTICE_MARKER); - } else { - expect(joined).not.toContain(NOTICE_MARKER); - } +describe("e2e: Node upgrade notice on a real tools/call round-trip", () => { + afterEach(() => setNode(realNode)); - await client.close(); + it("appends the notice as the last block on simulated Node < 22", async () => { + setNode("18.20.8"); + const blocks = await callListTestCases(); + const joined = blocks.map((c) => c.text ?? "").join("\n"); + expect(joined).toContain(NOTICE_MARKER); + expect(blocks[blocks.length - 1].text).toContain(NOTICE_MARKER); // payload stays first + }); + + it("omits the notice on simulated Node >= 22", async () => { + setNode("22.18.0"); + const blocks = await callListTestCases(); + const joined = blocks.map((c) => c.text ?? "").join("\n"); + expect(joined).not.toContain(NOTICE_MARKER); }); }); From 51a18cf0c05a99ddb0ab0a097c4203c2824ffca1 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Wed, 23 Sep 2026 18:44:56 +0530 Subject: [PATCH 12/13] refactor(security): deliver Node nudge via server instructions (per review) Address Ruturaj's review: - Revert oninitialized.ts to the original Node <18 throw guard (unchanged). - Replace the server.tool monkey-patch with the MCP server `instructions` field: instructions = nodeUpgradeNotice() || undefined. Simpler, one place; a temporary bridge to be removed with the lazy-load in ~2 weeks. - Drop the now-unused withNodeUpgradeNotice helper. - Tests: notice now asserted via client.getInstructions(); remove the obsolete oninitialized boundary test. Co-Authored-By: Claude Opus 4.8 --- tests/oninitialized.test.ts | 41 ------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 tests/oninitialized.test.ts diff --git a/tests/oninitialized.test.ts b/tests/oninitialized.test.ts deleted file mode 100644 index 621fd513..00000000 --- a/tests/oninitialized.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach, Mock } from "vitest"; -import logger from "../src/logger"; -import { setupOnInitialized } from "../src/oninitialized"; - -vi.mock("../src/logger", () => ({ - default: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }, -})); -vi.mock("../src/lib/instrumentation", () => ({ trackMCP: vi.fn() })); -vi.mock("../src/lib/device-cache", () => ({ shouldSendStartedEvent: () => false })); - -// Minimal McpServer stub — setupOnInitialized only sets server.server.oninitialized. -const makeServer = () => - ({ server: { oninitialized: undefined, getClientVersion: () => ({}) } }) as any; - -// process.versions.node is read-only; override it per-case then restore. -const realNode = process.versions.node; -const setNode = (v: string) => - Object.defineProperty(process.versions, "node", { value: v, configurable: true }); - -describe("setupOnInitialized – Node version nudge", () => { - beforeEach(() => vi.clearAllMocks()); - afterEach(() => setNode(realNode)); - - it.each(["18.19.0", "20.9.0", "21.7.3", "16.20.0"])( - "warns on Node < 22 (%s) without throwing", - (v) => { - setNode(v); - expect(() => setupOnInitialized(makeServer())).not.toThrow(); - expect(logger.warn as Mock).toHaveBeenCalledTimes(1); - }, - ); - - it.each(["22.0.0", "22.11.0", "24.3.0", "26.1.0"])( - "does not warn on Node >= 22 (%s)", - (v) => { - setNode(v); - setupOnInitialized(makeServer()); - expect(logger.warn as Mock).not.toHaveBeenCalled(); - }, - ); -}); From c4a8517d06816ccd116024bfa046e5b8af4347e4 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Wed, 23 Sep 2026 18:45:24 +0530 Subject: [PATCH 13/13] refactor(security): deliver Node nudge via server instructions (per review) - Revert oninitialized.ts to the original Node <18 throw guard (unchanged). - Replace the server.tool monkey-patch with the MCP server `instructions` field: instructions = nodeUpgradeNotice() || undefined. Simpler, one place; a temporary bridge to be removed with the lazy-load in ~2 weeks. - Drop the now-unused withNodeUpgradeNotice helper. - Tests: notice asserted via client.getInstructions(). Co-Authored-By: Claude Opus 4.8 --- src/lib/node-version-notice.ts | 13 --------- src/oninitialized.ts | 12 ++++---- src/server-factory.ts | 39 ++++++-------------------- tests/e2e/node-notice.e2e.test.ts | 40 ++++++++------------------- tests/lib/node-version-notice.test.ts | 26 +---------------- 5 files changed, 27 insertions(+), 103 deletions(-) diff --git a/src/lib/node-version-notice.ts b/src/lib/node-version-notice.ts index 044d9b88..eb582c9a 100644 --- a/src/lib/node-version-notice.ts +++ b/src/lib/node-version-notice.ts @@ -1,5 +1,3 @@ -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; - const RECOMMENDED_NODE_MAJOR = 22; export function nodeUpgradeNotice( @@ -13,14 +11,3 @@ export function nodeUpgradeNotice( `Node ${version}; older versions will be unsupported.` ); } - -export function withNodeUpgradeNotice( - result: T, - notice: string = nodeUpgradeNotice(), -): T { - if (!notice || !result || !Array.isArray(result.content)) return result; - return { - ...result, - content: [...result.content, { type: "text", text: notice }], - }; -} diff --git a/src/oninitialized.ts b/src/oninitialized.ts index 8e8617ff..1aa04205 100644 --- a/src/oninitialized.ts +++ b/src/oninitialized.ts @@ -1,13 +1,15 @@ import { trackMCP } from "./lib/instrumentation.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { shouldSendStartedEvent } from "./lib/device-cache.js"; -import logger from "./logger.js"; -import { nodeUpgradeNotice } from "./lib/node-version-notice.js"; export function setupOnInitialized(server: McpServer, config?: any) { - const notice = nodeUpgradeNotice(); - if (notice) { - logger.warn(notice); + const nodeVersion = process.versions.node; + + // Check for Node.js version + if (nodeVersion < "18.0.0") { + throw new Error( + "Node version is not supported. Please upgrade to 18.0.0 or later.", + ); } server.server.oninitialized = () => { diff --git a/src/server-factory.ts b/src/server-factory.ts index 209c5f1d..47d22f4f 100644 --- a/src/server-factory.ts +++ b/src/server-factory.ts @@ -21,10 +21,7 @@ import { setupOnInitialized } from "./oninitialized.js"; import { BrowserStackConfig } from "./lib/types.js"; import addRCATools from "./tools/rca-agent.js"; import addAskBrowserStackAITool from "./tools/ask-browserstack/register.js"; -import { - nodeUpgradeNotice, - withNodeUpgradeNotice, -} from "./lib/node-version-notice.js"; +import { nodeUpgradeNotice } from "./lib/node-version-notice.js"; /** * Wrapper class for BrowserStack MCP Server @@ -40,38 +37,18 @@ export class BrowserStackMcpServer { packageJson.version, ); - this.server = new McpServer({ - name: "BrowserStack MCP Server", - version: packageJson.version, - }); + this.server = new McpServer( + { + name: "BrowserStack MCP Server", + version: packageJson.version, + }, + { instructions: nodeUpgradeNotice() || undefined }, + ); setupOnInitialized(this.server, this.config); - this.applyNodeUpgradeNotice(); this.registerTools(); } - private applyNodeUpgradeNotice() { - const notice = nodeUpgradeNotice(); - if (!notice) return; - - const server = this.server; - const originalTool = server.tool.bind(server) as ( - ...args: unknown[] - ) => RegisteredTool; - - ( - server as unknown as { tool: (...args: unknown[]) => RegisteredTool } - ).tool = (...args: unknown[]): RegisteredTool => { - const lastIndex = args.length - 1; - const cb = args[lastIndex]; - if (typeof cb === "function") { - args[lastIndex] = async (...cbArgs: unknown[]) => - withNodeUpgradeNotice(await cb(...cbArgs), notice); - } - return originalTool(...args); - }; - } - /** * Calls each tool-adder function and collects their returned tools */ diff --git a/tests/e2e/node-notice.e2e.test.ts b/tests/e2e/node-notice.e2e.test.ts index fffdd564..c2cd4e51 100644 --- a/tests/e2e/node-notice.e2e.test.ts +++ b/tests/e2e/node-notice.e2e.test.ts @@ -1,16 +1,6 @@ import { describe, it, expect, vi, afterEach } from "vitest"; -// Keep the tool off the network so we exercise the response path, not the API. vi.mock("../../src/lib/instrumentation", () => ({ trackMCP: vi.fn() })); -vi.mock("../../src/lib/tm-base-url", () => ({ - getTMBaseURL: vi.fn().mockResolvedValue("https://tm.example.com"), -})); -vi.mock("../../src/lib/apiClient", () => ({ - apiClient: { - get: vi.fn().mockResolvedValue({ data: { success: true, test_cases: [] } }), - post: vi.fn().mockResolvedValue({ data: { success: true } }), - }, -})); import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; @@ -19,8 +9,7 @@ import { BrowserStackMcpServer } from "../../src/server-factory"; const NOTICE_MARKER = "Node version > 21.x.x"; // process.versions.node is read-only; override per-case then restore so both -// the wrapped (<22) and unchanged (>=22) registration paths run deterministically -// regardless of the host Node version. +// the below-22 and 22-or-newer paths run deterministically on any host Node. const realNode = process.versions.node; const setNode = (v: string) => Object.defineProperty(process.versions, "node", { @@ -28,12 +17,12 @@ const setNode = (v: string) => configurable: true, }); -async function callListTestCases(): Promise> { +async function serverInstructions(): Promise { const config: any = { "browserstack-username": "u", "browserstack-access-key": "k", }; - const bs = new BrowserStackMcpServer(config); // applies wrapper based on faked node + const bs = new BrowserStackMcpServer(config); // sets instructions from faked node const [clientT, serverT] = InMemoryTransport.createLinkedPair(); const client = new Client( { name: "e2e", version: "1.0.0" }, @@ -43,29 +32,22 @@ async function callListTestCases(): Promise> { client.connect(clientT), bs.getInstance().connect(serverT), ]); - const res: any = await client.callTool({ - name: "listTestCases", - arguments: { project_identifier: "PR-1" }, - }); + const instructions = client.getInstructions(); await client.close(); - return res.content ?? []; + return instructions; } -describe("e2e: Node upgrade notice on a real tools/call round-trip", () => { +describe("e2e: Node upgrade notice via server instructions (server prompt)", () => { afterEach(() => setNode(realNode)); - it("appends the notice as the last block on simulated Node < 22", async () => { + it("includes the notice in server instructions on simulated Node < 22", async () => { setNode("18.20.8"); - const blocks = await callListTestCases(); - const joined = blocks.map((c) => c.text ?? "").join("\n"); - expect(joined).toContain(NOTICE_MARKER); - expect(blocks[blocks.length - 1].text).toContain(NOTICE_MARKER); // payload stays first + expect(await serverInstructions()).toContain(NOTICE_MARKER); }); - it("omits the notice on simulated Node >= 22", async () => { + it("omits it (empty instructions) on simulated Node >= 22", async () => { setNode("22.18.0"); - const blocks = await callListTestCases(); - const joined = blocks.map((c) => c.text ?? "").join("\n"); - expect(joined).not.toContain(NOTICE_MARKER); + const instructions = await serverInstructions(); + expect(instructions ?? "").not.toContain(NOTICE_MARKER); }); }); diff --git a/tests/lib/node-version-notice.test.ts b/tests/lib/node-version-notice.test.ts index 2734860d..dce599ef 100644 --- a/tests/lib/node-version-notice.test.ts +++ b/tests/lib/node-version-notice.test.ts @@ -1,8 +1,5 @@ import { describe, it, expect } from "vitest"; -import { - nodeUpgradeNotice, - withNodeUpgradeNotice, -} from "../../src/lib/node-version-notice"; +import { nodeUpgradeNotice } from "../../src/lib/node-version-notice"; describe("nodeUpgradeNotice", () => { it.each(["18.19.0", "20.9.0", "21.7.3"])( @@ -20,24 +17,3 @@ describe("nodeUpgradeNotice", () => { }, ); }); - -describe("withNodeUpgradeNotice", () => { - const result = { content: [{ type: "text", text: "original" }] }; - - it("appends the notice when one applies (payload stays at content[0])", () => { - const out = withNodeUpgradeNotice(result, "⚠️ upgrade"); - expect(out.content).toHaveLength(2); - expect(out.content[0]).toEqual({ type: "text", text: "original" }); - expect(out.content[1]).toEqual({ type: "text", text: "⚠️ upgrade" }); - }); - - it("returns the result unchanged when the notice is empty (Node >= 22)", () => { - const out = withNodeUpgradeNotice(result, ""); - expect(out).toBe(result); - }); - - it("is a no-op on malformed results", () => { - const bad = {} as any; - expect(withNodeUpgradeNotice(bad, "⚠️ upgrade")).toBe(bad); - }); -});