From a932a0b2c24aeefa0c48ea3a03a2268e7a2b646c Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 16:02:24 +0700 Subject: [PATCH 01/12] chore: initialize MCP connector package --- .gitignore | 3 + AGENTS.md | 3 + LICENSE | 21 + SECURITY.md | 11 + package-lock.json | 1014 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 46 ++ 6 files changed, 1098 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dce4419 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*.tgz +pack-result.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2d1d770 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +# Repository boundary + +This repository owns only the `@agentcommunity/mcp` stdio connector. The hosted server and its tools live at `https://agentcommunity.org/mcp`; do not copy server logic here. The connector must remain anonymous, fixed-origin, telemetry-free, and dependency-pinned. It must never call a tool during tests or installation, expose an SDK API, accept an alternate remote URL, add install scripts, or store credentials. The sole local-publish exception is the reviewed `0.1.0` bootstrap required to create the npm package before trusted publishing can be attached; it must be deprecated after `0.1.1` is live. Every later publication, including canonical `0.1.1`, uses the tag-only OIDC workflow after `npm test` and `npm run package:audit` pass. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..040a272 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Agent Community + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..fb9bb6f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported versions + +The `0.1.x` release line is supported with security updates. + +## Reporting a vulnerability + +Report vulnerabilities privately to [security@agentcommunity.org](mailto:security@agentcommunity.org). + +Do not include secrets, credentials, private keys, tokens, or other sensitive information in public GitHub issues. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..61cb5af --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1014 @@ +{ + "name": "@agentcommunity/mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@agentcommunity/mcp", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "mcp-remote": "0.1.38" + }, + "bin": { + "agentcommunity-mcp": "bin.js" + }, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-apply-helpers": { + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "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/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/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.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "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-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mcp-remote": { + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/mcp-remote/-/mcp-remote-0.1.38.tgz", + "integrity": "sha512-w+JU4U3CfG29TawXR4JLNQ9d1Un5nT8AGI65f/juCaqUdF/V6fS7wE4o7xNPbB8X58o46hRXEJgYglQMAKQs4w==", + "license": "MIT", + "dependencies": { + "express": "^4.21.2", + "open": "^10.1.0", + "strict-url-sanitise": "^0.0.1", + "undici": "^7.12.0" + }, + "bin": { + "mcp-remote": "dist/proxy.js", + "mcp-remote-client": "dist/client.js" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "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" + }, + "funding": { + "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/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "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/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "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/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/strict-url-sanitise": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/strict-url-sanitise/-/strict-url-sanitise-0.0.1.tgz", + "integrity": "sha512-nuFtF539K8jZg3FjaWH/L8eocCR6gegz5RDOsaWxfdbF5Jqr2VXWxZayjTwUzsWJDC91k2EbnJXp6FuWW+Z4hg==", + "license": "MIT" + }, + "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/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.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/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..70e7990 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "@agentcommunity/mcp", + "version": "0.1.0", + "description": "Official Agent Community MCP connector for stdio clients; bridges to the hosted Streamable HTTP server at https://agentcommunity.org/mcp", + "type": "module", + "bin": { + "agentcommunity-mcp": "bin.js" + }, + "files": [ + "bin.js", + "README.md", + "LICENSE", + "SECURITY.md" + ], + "scripts": { + "test": "node --test", + "package:audit": "node scripts/audit-package.mjs" + }, + "engines": { + "node": ">=20.18.1" + }, + "keywords": [ + "agentcommunity", + "agent-community", + "mcp", + "model-context-protocol", + "stdio", + "streamable-http", + "ai-agents" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/agentcommunity/mcp.git" + }, + "homepage": "https://agentcommunity.org/developers", + "bugs": { + "url": "https://github.com/agentcommunity/mcp/issues" + }, + "license": "MIT", + "dependencies": { + "mcp-remote": "0.1.38" + } +} From 8a59c819120274d39016ac5dfb5924d6c2cfc9de Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 16:12:37 +0700 Subject: [PATCH 02/12] feat: add fixed-origin stdio MCP connector --- README.md | 22 +++++++++++-- bin.js | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ test/bin.test.js | 68 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 2 deletions(-) create mode 100755 bin.js create mode 100644 test/bin.test.js diff --git a/README.md b/README.md index 5146de7..2c7af14 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ -# mcp -Official stdio connector for the Agent Community hosted MCP server +# Agent Community MCP connector + +`@agentcommunity/mcp` is the official stdio compatibility connector for Agent Community's hosted MCP server at `https://agentcommunity.org/mcp`. It is published by Agent Community from `https://github.com/agentcommunity/mcp`. + +It does not implement or copy the server, expose an SDK API, add authentication, or call a tool during startup. It launches the pinned `mcp-remote` transport bridge so stdio-only clients can use the existing anonymous Streamable HTTP endpoint. + +## Run + +```sh +npx -y @agentcommunity/mcp +``` + +The installed executable is `agentcommunity-mcp`. Clients with native remote Streamable HTTP support should connect directly to `https://agentcommunity.org/mcp` instead. + +## Verify ownership + +- Homepage and documentation: https://agentcommunity.org/developers +- Source: https://github.com/agentcommunity/mcp +- Package: https://www.npmjs.com/package/@agentcommunity/mcp +- Hosted server card: https://agentcommunity.org/.well-known/mcp/server-card.json diff --git a/bin.js b/bin.js new file mode 100755 index 0000000..d0481e6 --- /dev/null +++ b/bin.js @@ -0,0 +1,83 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const remoteManifestPath = require.resolve('mcp-remote/package.json'); +const remoteManifest = JSON.parse(readFileSync(remoteManifestPath, 'utf8')); +const remoteBin = remoteManifest.bin?.['mcp-remote']; + +if (typeof remoteBin !== 'string') { + throw new Error('Installed mcp-remote package does not expose its expected executable.'); +} + +const remoteExecutable = join(dirname(remoteManifestPath), remoteBin); +const remoteUrl = 'https://agentcommunity.org/mcp'; + +export const helpText = `Official Agent Community MCP connector + +Usage: agentcommunity-mcp [mcp-remote options] + +Bridges a local stdio MCP client to ${remoteUrl}. +The hosted endpoint is anonymous; this connector stores no credentials. +`; + +export function buildInvocation(argv = process.argv.slice(2)) { + return { + command: process.execPath, + args: [remoteExecutable, remoteUrl, ...argv], + }; +} + +const terminationSignals = ['SIGINT', 'SIGTERM', 'SIGHUP']; + +export function installSignalForwarding(child, hostProcess = process) { + const handlers = new Map(); + for (const signal of terminationSignals) { + const handler = function () { + child.kill(signal); + }; + handlers.set(signal, handler); + hostProcess.on(signal, handler); + } + return function removeSignalForwarding() { + for (const [signal, handler] of handlers) hostProcess.off(signal, handler); + }; +} + +export function applyChildExit(code, signal, hostProcess = process) { + if (signal !== null) { + hostProcess.kill(hostProcess.pid, signal); + return; + } + hostProcess.exitCode = code ?? 1; +} + +export function main(argv = process.argv.slice(2)) { + if (argv.includes('--help') || argv.includes('-h')) { + process.stdout.write(helpText); + return; + } + + const invocation = buildInvocation(argv); + const child = spawn(invocation.command, invocation.args, { + stdio: 'inherit', + env: process.env, + }); + const removeSignalForwarding = installSignalForwarding(child); + + child.once('error', function (error) { + removeSignalForwarding(); + process.stderr.write(`agentcommunity-mcp: ${error.message}\n`); + process.exitCode = 1; + }); + child.once('exit', function (code, signal) { + removeSignalForwarding(); + applyChildExit(code, signal); + }); +} + +if (fileURLToPath(import.meta.url) === process.argv[1]) main(); diff --git a/test/bin.test.js b/test/bin.test.js new file mode 100644 index 0000000..c00a49e --- /dev/null +++ b/test/bin.test.js @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { EventEmitter } from 'node:events'; +import test from 'node:test'; + +import { applyChildExit, buildInvocation, helpText, installSignalForwarding } from '../bin.js'; + +test('pins the hosted Agent Community MCP origin before forwarded client arguments', function () { + const invocation = buildInvocation(['--transport', 'http-only']); + + assert.equal(invocation.command, process.execPath); + assert.match(invocation.args[0], /node_modules[/\\]mcp-remote[/\\]dist[/\\]proxy\.js$/); + assert.deepEqual(invocation.args.slice(1), [ + 'https://agentcommunity.org/mcp', + '--transport', + 'http-only', + ]); +}); + +test('help describes a connector and never claims to own the server or SDK', function () { + assert.match(helpText, /Official Agent Community MCP connector/); + assert.match(helpText, /https:\/\/agentcommunity\.org\/mcp/); + assert.doesNotMatch(helpText, /SDK|implements four tools|MCP server package/); +}); + +test('package metadata has no install scripts or SDK export', async function () { + const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')); + + assert.equal(manifest.name, '@agentcommunity/mcp'); + assert.equal(manifest.bin['agentcommunity-mcp'], 'bin.js'); + assert.equal(manifest.dependencies['mcp-remote'], '0.1.38'); + assert.equal(manifest.exports, undefined); + assert.equal(manifest.scripts.preinstall, undefined); + assert.equal(manifest.scripts.install, undefined); + assert.equal(manifest.scripts.postinstall, undefined); +}); + +test('forwards host termination signals to the child and removes listeners', function () { + const host = new EventEmitter(); + const forwarded = []; + const child = { + killed: false, + kill: function (signal) { + forwarded.push(signal); + this.killed = true; + }, + }; + const remove = installSignalForwarding(child, host); + + host.emit('SIGINT'); + host.emit('SIGTERM'); + host.emit('SIGHUP'); + assert.deepEqual(forwarded, ['SIGINT', 'SIGTERM', 'SIGHUP']); + + remove(); + host.emit('SIGTERM'); + assert.deepEqual(forwarded, ['SIGINT', 'SIGTERM', 'SIGHUP']); +}); + +test('mirrors ordinary exit codes and child termination signals', function () { + const kills = []; + const host = { pid: 42, exitCode: undefined, kill: function (pid, signal) { kills.push([pid, signal]); } }; + + applyChildExit(7, null, host); + assert.equal(host.exitCode, 7); + applyChildExit(null, 'SIGTERM', host); + assert.deepEqual(kills, [[42, 'SIGTERM']]); +}); From 0cb8fdd44ba117f8d457d03f8e7daeaf7c045634 Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 16:24:36 +0700 Subject: [PATCH 03/12] fix: harden installed MCP connector entrypoint --- bin.js | 14 ++++++++-- package.json | 1 + test/bin.test.js | 68 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/bin.js b/bin.js index d0481e6..e1327dd 100755 --- a/bin.js +++ b/bin.js @@ -1,6 +1,6 @@ #!/usr/bin/env node import { spawn } from 'node:child_process'; -import { readFileSync } from 'node:fs'; +import { readFileSync, realpathSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -80,4 +80,14 @@ export function main(argv = process.argv.slice(2)) { }); } -if (fileURLToPath(import.meta.url) === process.argv[1]) main(); +function isMainModule() { + if (typeof process.argv[1] !== 'string') return false; + + try { + return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]); + } catch { + return false; + } +} + +if (isMainModule()) main(); diff --git a/package.json b/package.json index 70e7990..3b06ad8 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "description": "Official Agent Community MCP connector for stdio clients; bridges to the hosted Streamable HTTP server at https://agentcommunity.org/mcp", "type": "module", + "exports": {}, "bin": { "agentcommunity-mcp": "bin.js" }, diff --git a/test/bin.test.js b/test/bin.test.js index c00a49e..c7e5752 100644 --- a/test/bin.test.js +++ b/test/bin.test.js @@ -1,10 +1,47 @@ import assert from 'node:assert/strict'; -import { readFile } from 'node:fs/promises'; +import { execFile, spawnSync } from 'node:child_process'; import { EventEmitter } from 'node:events'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import test from 'node:test'; +import { promisify } from 'node:util'; +import { fileURLToPath } from 'node:url'; import { applyChildExit, buildInvocation, helpText, installSignalForwarding } from '../bin.js'; +const execFileAsync = promisify(execFile); +const packageRoot = fileURLToPath(new URL('../', import.meta.url)); +const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + +async function withPackedInstall(callback) { + const temporaryRoot = await mkdtemp(join(tmpdir(), 'agentcommunity-mcp-')); + const packDirectory = join(temporaryRoot, 'pack'); + const clientDirectory = join(temporaryRoot, 'client'); + + try { + await mkdir(packDirectory); + await mkdir(clientDirectory); + await writeFile(join(clientDirectory, 'package.json'), '{"private":true,"type":"module"}\n'); + + const packed = await execFileAsync( + npmCommand, + ['pack', '--json', '--pack-destination', packDirectory], + { cwd: packageRoot }, + ); + const [{ filename }] = JSON.parse(packed.stdout); + await execFileAsync( + npmCommand, + ['install', '--ignore-scripts', '--no-audit', '--no-fund', join(packDirectory, filename)], + { cwd: clientDirectory }, + ); + + await callback(clientDirectory); + } finally { + await rm(temporaryRoot, { recursive: true, force: true }); + } +} + test('pins the hosted Agent Community MCP origin before forwarded client arguments', function () { const invocation = buildInvocation(['--transport', 'http-only']); @@ -23,18 +60,45 @@ test('help describes a connector and never claims to own the server or SDK', fun assert.doesNotMatch(helpText, /SDK|implements four tools|MCP server package/); }); +test('packed package emits help through the installed npm shim', async function () { + await withPackedInstall(async function (clientDirectory) { + const shimName = process.platform === 'win32' + ? 'agentcommunity-mcp.cmd' + : 'agentcommunity-mcp'; + const shimPath = join(clientDirectory, 'node_modules', '.bin', shimName); + const result = await execFileAsync(shimPath, ['--help'], { cwd: clientDirectory }); + + assert.match(result.stdout, /Official Agent Community MCP connector/); + assert.match(result.stdout, /https:\/\/agentcommunity\.org\/mcp/); + assert.equal(result.stderr, ''); + }); +}); + test('package metadata has no install scripts or SDK export', async function () { const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')); assert.equal(manifest.name, '@agentcommunity/mcp'); assert.equal(manifest.bin['agentcommunity-mcp'], 'bin.js'); assert.equal(manifest.dependencies['mcp-remote'], '0.1.38'); - assert.equal(manifest.exports, undefined); + assert.deepEqual(manifest.exports, {}); assert.equal(manifest.scripts.preinstall, undefined); assert.equal(manifest.scripts.install, undefined); assert.equal(manifest.scripts.postinstall, undefined); }); +test('packed package rejects deep imports across the no-SDK boundary', async function () { + await withPackedInstall(async function (clientDirectory) { + const result = spawnSync( + process.execPath, + ['--input-type=module', '--eval', "await import('@agentcommunity/mcp/bin.js')"], + { cwd: clientDirectory, encoding: 'utf8' }, + ); + + assert.equal(result.status, 1); + assert.match(result.stderr, /ERR_PACKAGE_PATH_NOT_EXPORTED/); + }); +}); + test('forwards host termination signals to the child and removes listeners', function () { const host = new EventEmitter(); const forwarded = []; From 09e496fd02ade04e6efbbbae82a8dda468ee0977 Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 16:38:19 +0700 Subject: [PATCH 04/12] test(package): audit MCP release artifact --- scripts/audit-package.mjs | 256 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 scripts/audit-package.mjs diff --git a/scripts/audit-package.mjs b/scripts/audit-package.mjs new file mode 100644 index 0000000..36d66a2 --- /dev/null +++ b/scripts/audit-package.mjs @@ -0,0 +1,256 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { lstat, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { basename, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const expectedName = '@agentcommunity/mcp'; +const expectedFiles = ['LICENSE', 'README.md', 'SECURITY.md', 'bin.js', 'package.json']; +const expectedBin = { 'agentcommunity-mcp': 'bin.js' }; +const expectedKeywords = [ + 'agentcommunity', + 'agent-community', + 'mcp', + 'model-context-protocol', + 'stdio', + 'streamable-http', + 'ai-agents', +]; +const expectedHomepage = 'https://agentcommunity.org/developers'; +const expectedRepository = { + type: 'git', + url: 'git+https://github.com/agentcommunity/mcp.git', +}; +const expectedEngines = { node: '>=20.18.1' }; +const expectedDependencies = { 'mcp-remote': '0.1.38' }; +const installedShimRelativePath = 'node_modules/.bin/agentcommunity-mcp'; +const hostedMcpUrl = 'https://agentcommunity.org/mcp'; +const brandedHeading = 'Official Agent Community MCP connector'; +const secretPatterns = [ + { label: 'private key', pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/ }, + { label: 'npm token', pattern: /\bnpm_[A-Za-z0-9]{20,}\b/ }, + { label: 'GitHub token', pattern: /\bgh[opusr]_[A-Za-z0-9]{20,}\b/ }, + { label: 'GitHub token', pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/ }, + { label: 'Stripe key', pattern: /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}\b/ }, + { + label: 'JWT', + pattern: /\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\b/, + }, +]; + +function isRecord(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function command(commandName, args, cwd) { + const result = spawnSync(commandName, args, { + cwd, + encoding: 'utf8', + maxBuffer: 16 * 1024 * 1024, + }); + + if (result.error !== undefined) throw result.error; + if (result.status !== 0) { + throw new Error( + `${commandName} ${args.join(' ')} failed with exit ${result.status}:\n${result.stderr}`, + ); + } + + return result.stdout; +} + +function npmCommand(args, cwd) { + const npmExecPath = process.env.npm_execpath; + + return npmExecPath === undefined + ? command(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, cwd) + : command(process.execPath, [npmExecPath, ...args], cwd); +} + +function invalidPackOutput(message) { + throw new Error(`Invalid npm pack JSON output: ${message}.`); +} + +function normalizePackResult(value, expectedVersion) { + let candidate; + + if (Array.isArray(value)) { + if (value.length !== 1) invalidPackOutput('expected exactly one result'); + [candidate] = value; + } else if (isRecord(value)) { + const entries = Object.entries(value); + if (entries.length !== 1 || entries[0][0] !== expectedName) { + invalidPackOutput('expected exactly one result keyed by the package name'); + } + candidate = entries[0][1]; + } else { + invalidPackOutput('top-level value is not an array or package map'); + } + + if (!isRecord(candidate)) invalidPackOutput('result is not an object'); + if (candidate.name !== expectedName || candidate.version !== expectedVersion) { + invalidPackOutput('result name or version does not match the source manifest'); + } + + const expectedFilename = `agentcommunity-mcp-${expectedVersion}.tgz`; + if ( + candidate.filename !== expectedFilename + || basename(candidate.filename) !== candidate.filename + ) { + invalidPackOutput('tarball filename is not the expected package filename'); + } + if (!Array.isArray(candidate.files)) invalidPackOutput('result files are missing'); + if (candidate.files.some(function (file) { + return !isRecord(file) + || typeof file.path !== 'string' + || file.path.length === 0 + || typeof file.size !== 'number' + || !Number.isSafeInteger(file.size) + || file.size < 0; + })) { + invalidPackOutput('result files are malformed'); + } + + return candidate; +} + +function assertExactManifest(manifest, expectedVersion, source) { + assert.equal(manifest.name, expectedName, `${source} package name drift detected`); + assert.equal(manifest.version, expectedVersion, `${source} package version drift detected`); + assert.deepEqual(manifest.bin, expectedBin, `${source} binary mapping drift detected`); + assert.deepEqual(manifest.keywords, expectedKeywords, `${source} keywords drift detected`); + assert.equal(manifest.homepage, expectedHomepage, `${source} homepage drift detected`); + assert.deepEqual( + manifest.repository, + expectedRepository, + `${source} repository metadata drift detected`, + ); + assert.equal(manifest.license, 'MIT', `${source} license drift detected`); + assert.deepEqual(manifest.engines, expectedEngines, `${source} Node engine drift detected`); + assert.deepEqual( + manifest.dependencies, + expectedDependencies, + `${source} dependency boundary drift detected`, + ); + assert.deepEqual(manifest.exports, {}, `${source} exports must be an exact empty map`); + + for (const scriptName of ['preinstall', 'install', 'postinstall']) { + assert.equal( + manifest.scripts?.[scriptName], + undefined, + `${source} manifest must not contain a ${scriptName} script`, + ); + } +} + +async function main() { + const repositoryRoot = fileURLToPath(new URL('../', import.meta.url)); + const sourceManifest = JSON.parse(await readFile(join(repositoryRoot, 'package.json'), 'utf8')); + if (typeof sourceManifest.version !== 'string' || sourceManifest.version.length === 0) { + throw new Error('Source package version is missing.'); + } + assertExactManifest(sourceManifest, sourceManifest.version, 'Source'); + + let packDirectory; + let installDirectory; + + try { + packDirectory = await mkdtemp(join(tmpdir(), 'agentcommunity-mcp-pack-')); + installDirectory = await mkdtemp(join(tmpdir(), 'agentcommunity-mcp-install-')); + + const packOutput = npmCommand( + ['pack', '--json', '--pack-destination', packDirectory], + repositoryRoot, + ); + const packResult = normalizePackResult(JSON.parse(packOutput), sourceManifest.version); + const inventory = packResult.files.map(function (file) { return file.path; }).sort(); + assert.deepEqual(inventory, expectedFiles, 'Unexpected package inventory'); + + const tarballPath = join(packDirectory, packResult.filename); + const packedManifest = JSON.parse( + command('tar', ['-xOf', tarballPath, 'package/package.json'], repositoryRoot), + ); + assertExactManifest(packedManifest, sourceManifest.version, 'Packed tarball'); + + for (const packedPath of inventory) { + const content = command( + 'tar', + ['-xOf', tarballPath, `package/${packedPath}`], + repositoryRoot, + ); + for (const { label, pattern } of secretPatterns) { + if (pattern.test(content)) throw new Error(`Possible ${label} in packed file ${packedPath}.`); + } + } + + const tarball = await readFile(tarballPath); + const tarballSha256 = createHash('sha256').update(tarball).digest('hex'); + + await writeFile( + join(installDirectory, 'package.json'), + '{"name":"agentcommunity-mcp-clean-install","private":true,"version":"1.0.0"}\n', + ); + npmCommand( + ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarballPath], + installDirectory, + ); + + const installedPackageRoot = join( + installDirectory, + 'node_modules', + '@agentcommunity', + 'mcp', + ); + const installedManifest = JSON.parse( + await readFile(join(installedPackageRoot, 'package.json'), 'utf8'), + ); + assertExactManifest(installedManifest, sourceManifest.version, 'Installed package'); + + const installedShim = join(installDirectory, installedShimRelativePath); + assert.equal( + (await lstat(installedShim)).isSymbolicLink(), + true, + 'Clean-install executable shim is not a symbolic link', + ); + const installedShimTarget = await realpath(installedShim); + const expectedShimTarget = await realpath( + join(installedPackageRoot, expectedBin['agentcommunity-mcp']), + ); + assert.equal( + installedShimTarget, + expectedShimTarget, + 'Clean-install executable shim does not resolve to the packed bin.js', + ); + + const help = spawnSync(installedShim, ['--help'], { + cwd: installDirectory, + encoding: 'utf8', + maxBuffer: 1024 * 1024, + }); + if (help.error !== undefined) throw help.error; + assert.equal(help.status, 0, `Clean-install help smoke exited ${help.status}:\n${help.stderr}`); + assert.ok(help.stdout.includes(brandedHeading), 'Connector heading is missing from help'); + assert.ok(help.stdout.includes(hostedMcpUrl), 'Hosted MCP URL is missing from help'); + + process.stdout.write(`${JSON.stringify({ + filename: packResult.filename, + sha256: tarballSha256, + files: inventory, + installed_shim_target: installedShimTarget, + clean_install_help: 'passed', + }, null, 2)}\n`); + } finally { + await Promise.all([ + packDirectory === undefined + ? Promise.resolve() + : rm(packDirectory, { recursive: true, force: true }), + installDirectory === undefined + ? Promise.resolve() + : rm(installDirectory, { recursive: true, force: true }), + ]); + } +} + +await main(); From d46779c1141c938cc9005114d04ca463773fc42a Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 16:50:13 +0700 Subject: [PATCH 05/12] fix(package): harden artifact audit portability --- scripts/audit-package.mjs | 134 +++++++++++++++++++++++++++++++------ test/audit-package.test.js | 75 +++++++++++++++++++++ 2 files changed, 189 insertions(+), 20 deletions(-) create mode 100644 test/audit-package.test.js diff --git a/scripts/audit-package.mjs b/scripts/audit-package.mjs index 36d66a2..b0b40f1 100644 --- a/scripts/audit-package.mjs +++ b/scripts/audit-package.mjs @@ -3,7 +3,7 @@ import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { lstat, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; -import { basename, join } from 'node:path'; +import { basename, dirname, join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const expectedName = '@agentcommunity/mcp'; @@ -25,7 +25,6 @@ const expectedRepository = { }; const expectedEngines = { node: '>=20.18.1' }; const expectedDependencies = { 'mcp-remote': '0.1.38' }; -const installedShimRelativePath = 'node_modules/.bin/agentcommunity-mcp'; const hostedMcpUrl = 'https://agentcommunity.org/mcp'; const brandedHeading = 'Official Agent Community MCP connector'; const secretPatterns = [ @@ -34,16 +33,109 @@ const secretPatterns = [ { label: 'GitHub token', pattern: /\bgh[opusr]_[A-Za-z0-9]{20,}\b/ }, { label: 'GitHub token', pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/ }, { label: 'Stripe key', pattern: /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}\b/ }, - { - label: 'JWT', - pattern: /\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\b/, - }, ]; function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } +function decodeBase64UrlJson(segment) { + try { + const decoded = Buffer.from(segment, 'base64url'); + if (decoded.toString('base64url') !== segment) return undefined; + return JSON.parse(decoded.toString('utf8')); + } catch { + return undefined; + } +} + +export function containsJwt(content) { + const candidatePattern = /(? 0 + && isRecord(payload) + && hasCanonicalSignature + ) { + return true; + } + } + + return false; +} + +export function installedShimName(platform = process.platform) { + return platform === 'win32' ? 'agentcommunity-mcp.cmd' : 'agentcommunity-mcp'; +} + +export async function validateInstalledShimTarget( + shimPath, + expectedTargetPath, + platform = process.platform, +) { + const expectedTarget = await realpath(expectedTargetPath); + + if (platform === 'win32') { + assert.equal( + (await lstat(shimPath)).isFile(), + true, + 'Clean-install Windows executable shim is not a regular wrapper file', + ); + const expectedReference = relative(dirname(shimPath), expectedTargetPath).replaceAll('\\', '/'); + const wrapperLines = (await readFile(shimPath, 'utf8')) + .replaceAll('\\', '/') + .split(/\r?\n/); + const invokesExpectedTarget = wrapperLines.some(function (line) { + return line.includes(expectedReference) + && /%dp0%/i.test(line) + && /%_prog%|\bnode(?:\.exe)?\b/i.test(line) + && line.includes('%*'); + }); + assert.equal( + invokesExpectedTarget, + true, + 'Clean-install Windows executable shim does not invoke the packed bin.js', + ); + return expectedTarget; + } + + assert.equal( + (await lstat(shimPath)).isSymbolicLink(), + true, + 'Clean-install executable shim is not a symbolic link', + ); + const resolvedShim = await realpath(shimPath); + assert.equal( + resolvedShim, + expectedTarget, + 'Clean-install executable shim does not resolve to the packed bin.js', + ); + return resolvedShim; +} + +export function buildShimHelpInvocation( + shimPath, + platform = process.platform, + environment = process.env, +) { + if (platform === 'win32') { + return { + command: environment.ComSpec ?? environment.COMSPEC ?? 'cmd.exe', + args: ['/d', '/s', '/c', `"${shimPath}" --help`], + }; + } + + return { command: shimPath, args: ['--help'] }; +} + function command(commandName, args, cwd) { const result = spawnSync(commandName, args, { cwd, @@ -183,6 +275,7 @@ async function main() { for (const { label, pattern } of secretPatterns) { if (pattern.test(content)) throw new Error(`Possible ${label} in packed file ${packedPath}.`); } + if (containsJwt(content)) throw new Error(`Possible JWT in packed file ${packedPath}.`); } const tarball = await readFile(tarballPath); @@ -208,23 +301,19 @@ async function main() { ); assertExactManifest(installedManifest, sourceManifest.version, 'Installed package'); - const installedShim = join(installDirectory, installedShimRelativePath); - assert.equal( - (await lstat(installedShim)).isSymbolicLink(), - true, - 'Clean-install executable shim is not a symbolic link', + const installedShim = join( + installDirectory, + 'node_modules', + '.bin', + installedShimName(), ); - const installedShimTarget = await realpath(installedShim); - const expectedShimTarget = await realpath( + const installedShimTarget = await validateInstalledShimTarget( + installedShim, join(installedPackageRoot, expectedBin['agentcommunity-mcp']), ); - assert.equal( - installedShimTarget, - expectedShimTarget, - 'Clean-install executable shim does not resolve to the packed bin.js', - ); - const help = spawnSync(installedShim, ['--help'], { + const helpInvocation = buildShimHelpInvocation(installedShim); + const help = spawnSync(helpInvocation.command, helpInvocation.args, { cwd: installDirectory, encoding: 'utf8', maxBuffer: 1024 * 1024, @@ -253,4 +342,9 @@ async function main() { } } -await main(); +function isMainModule() { + return typeof process.argv[1] === 'string' + && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +} + +if (isMainModule()) await main(); diff --git a/test/audit-package.test.js b/test/audit-package.test.js new file mode 100644 index 0000000..c6ce427 --- /dev/null +++ b/test/audit-package.test.js @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +const auditModulePromise = import('../scripts/audit-package.mjs'); + +test('validates and invokes the regular npm cmd shim on Windows', async function () { + const audit = await auditModulePromise; + assert.equal(typeof audit.installedShimName, 'function'); + assert.equal(typeof audit.validateInstalledShimTarget, 'function'); + assert.equal(typeof audit.buildShimHelpInvocation, 'function'); + + const temporaryRoot = await mkdtemp(join(tmpdir(), 'agentcommunity-mcp-windows-shim-')); + const shimDirectory = join(temporaryRoot, 'node_modules', '.bin'); + const packageDirectory = join(temporaryRoot, 'node_modules', '@agentcommunity', 'mcp'); + const shimPath = join(shimDirectory, 'agentcommunity-mcp.cmd'); + const targetPath = join(packageDirectory, 'bin.js'); + + try { + await mkdir(shimDirectory, { recursive: true }); + await mkdir(packageDirectory, { recursive: true }); + await writeFile(targetPath, '#!/usr/bin/env node\n'); + await writeFile( + shimPath, + '@ECHO off\r\n"%_prog%" "%dp0%\\..\\@agentcommunity\\mcp\\bin.js" %*\r\n', + ); + + assert.equal(audit.installedShimName('win32'), 'agentcommunity-mcp.cmd'); + assert.equal(audit.installedShimName('linux'), 'agentcommunity-mcp'); + assert.equal( + await audit.validateInstalledShimTarget(shimPath, targetPath, 'win32'), + await realpath(targetPath), + ); + assert.deepEqual( + audit.buildShimHelpInvocation(shimPath, 'win32', { ComSpec: 'C:\\Windows\\cmd.exe' }), + { + command: 'C:\\Windows\\cmd.exe', + args: ['/d', '/s', '/c', `"${shimPath}" --help`], + }, + ); + assert.deepEqual( + audit.buildShimHelpInvocation('/tmp/agentcommunity-mcp', 'linux', {}), + { command: '/tmp/agentcommunity-mcp', args: ['--help'] }, + ); + + await writeFile( + shimPath, + '@ECHO off\r\n"%_prog%" "%dp0%\\..\\other-package\\bin.js" %*\r\n', + ); + await assert.rejects( + audit.validateInstalledShimTarget(shimPath, targetPath, 'win32'), + /does not invoke the packed bin\.js/, + ); + } finally { + await rm(temporaryRoot, { recursive: true, force: true }); + } +}); + +test('detects a JWT with the minimal valid header without matching invalid JSON', async function () { + const audit = await auditModulePromise; + assert.equal(typeof audit.containsJwt, 'function'); + + const minimalHeaderToken = [ + 'eyJhbGciOiJIUzI1NiJ9', + 'eyJzdWIiOiIxIn0', + 'c2lnbmF0dXJl', + ].join('.'); + assert.equal(audit.containsJwt(minimalHeaderToken), true); + assert.equal( + audit.containsJwt('eyJhbGciOiJIUzI1NiJ9.bm90LWpzb24.c2lnbmF0dXJl'), + false, + ); +}); From 6a14726dc04627250a617094041df28d49067492 Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 16:54:30 +0700 Subject: [PATCH 06/12] fix(package): anchor Windows shim target --- scripts/audit-package.mjs | 15 +++++++++++---- test/audit-package.test.js | 9 +++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/scripts/audit-package.mjs b/scripts/audit-package.mjs index b0b40f1..970e79b 100644 --- a/scripts/audit-package.mjs +++ b/scripts/audit-package.mjs @@ -90,14 +90,21 @@ export async function validateInstalledShimTarget( 'Clean-install Windows executable shim is not a regular wrapper file', ); const expectedReference = relative(dirname(shimPath), expectedTargetPath).replaceAll('\\', '/'); + const expectedTargetArgument = `%dp0%/${expectedReference}`.toLowerCase(); const wrapperLines = (await readFile(shimPath, 'utf8')) .replaceAll('\\', '/') .split(/\r?\n/); const invokesExpectedTarget = wrapperLines.some(function (line) { - return line.includes(expectedReference) - && /%dp0%/i.test(line) - && /%_prog%|\bnode(?:\.exe)?\b/i.test(line) - && line.includes('%*'); + const quotedArguments = Array.from(line.matchAll(/"([^"\r\n]*)"/g), function (match) { + return match[1]; + }); + const programIndex = quotedArguments.findIndex(function (argument) { + return /^(?:%_prog%|node(?:\.exe)?)$/i.test(argument); + }); + + return programIndex !== -1 + && quotedArguments[programIndex + 1]?.toLowerCase() === expectedTargetArgument + && line.trimEnd().endsWith('%*'); }); assert.equal( invokesExpectedTarget, diff --git a/test/audit-package.test.js b/test/audit-package.test.js index c6ce427..59a72eb 100644 --- a/test/audit-package.test.js +++ b/test/audit-package.test.js @@ -45,6 +45,15 @@ test('validates and invokes the regular npm cmd shim on Windows', async function { command: '/tmp/agentcommunity-mcp', args: ['--help'] }, ); + await writeFile( + shimPath, + '@ECHO off\r\n"%_prog%" "%dp0%\\..\\@agentcommunity\\mcp\\bin.js.evil" %*\r\n', + ); + await assert.rejects( + audit.validateInstalledShimTarget(shimPath, targetPath, 'win32'), + /does not invoke the packed bin\.js/, + ); + await writeFile( shimPath, '@ECHO off\r\n"%_prog%" "%dp0%\\..\\other-package\\bin.js" %*\r\n', From 2b1da94fb72bf910f240b1b354772f3915c6fb36 Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 17:04:06 +0700 Subject: [PATCH 07/12] ci: add verified provenance release --- .github/workflows/ci.yml | 37 ++++++++++++++ .github/workflows/release.yml | 92 +++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9aadbc6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + test: + name: Node ${{ matrix.node }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + node: + - "20.18.1" + - "22" + - "24" + steps: + - name: Check out source + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + + - run: npm ci + - run: npm test + - run: npm run package:audit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1964330 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,92 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: read + id-token: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + publish: + name: Publish verified package + runs-on: ubuntu-24.04 + steps: + - name: Check out tagged source + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: "24" + registry-url: https://registry.npmjs.org + package-manager-cache: false + + - name: Use the trusted-publishing npm CLI + run: | + npm install --global npm@12.0.2 + test "$(npm --version)" = "12.0.2" + + - name: Require an exact stable release tag + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + node --input-type=module <<'NODE' + import { readFileSync } from "node:fs"; + + const manifest = JSON.parse(readFileSync("package.json", "utf8")); + if (!/^\d+\.\d+\.\d+$/.test(manifest.version)) { + throw new Error(`Release version must be stable SemVer, received ${manifest.version}`); + } + + const expectedTag = `v${manifest.version}`; + if (process.env.RELEASE_TAG !== expectedTag) { + throw new Error(`Tag ${process.env.RELEASE_TAG} does not match package version ${expectedTag}`); + } + NODE + + - run: npm ci + - run: npm test + - run: npm run package:audit + + - name: Pack the exact release artifact + id: pack + run: | + set -euo pipefail + npm pack --json > pack-result.json + tarball="$( + node --input-type=module <<'NODE' + import { readFileSync } from "node:fs"; + import { basename } from "node:path"; + + const manifest = JSON.parse(readFileSync("package.json", "utf8")); + const results = JSON.parse(readFileSync("pack-result.json", "utf8")); + if (!Array.isArray(results) || results.length !== 1) { + throw new Error(`Expected one packed artifact, received ${results.length}`); + } + + const [result] = results; + if (result.name !== manifest.name || result.version !== manifest.version) { + throw new Error("Packed artifact identity does not match package.json"); + } + if (typeof result.filename !== "string" || basename(result.filename) !== result.filename) { + throw new Error("Packed artifact filename is missing or unsafe"); + } + + process.stdout.write(result.filename); + NODE + )" + test -f "$tarball" + sha256sum "$tarball" + printf 'tarball=%s\n' "$tarball" >> "$GITHUB_OUTPUT" + + - name: Publish the exact artifact with OIDC provenance + run: npm publish "${{ steps.pack.outputs.tarball }}" --access public --provenance From 58afe21c2a7b5100e6a47173ee09ddb8af3fa847 Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 17:15:23 +0700 Subject: [PATCH 08/12] fix(ci): require release tag ancestry --- .github/workflows/release.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1964330..d77a6db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,8 +21,21 @@ jobs: - name: Check out tagged source uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: + fetch-depth: 0 persist-credentials: false + - name: Require the tagged commit to be on main + run: | + set -euo pipefail + if ! git show-ref --verify --quiet refs/remotes/origin/main; then + echo "origin/main was not fetched" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then + echo "Tagged commit $GITHUB_SHA is not an ancestor of origin/main" >&2 + exit 1 + fi + - name: Set up Node.js uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: From f36191cb1a624936979116f9d19dcb0123d746f7 Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 17:37:40 +0700 Subject: [PATCH 09/12] fix(release): audit the published tarball --- .github/workflows/release.yml | 42 ++++------ scripts/audit-package.mjs | 126 ++++++++++++++++++++++++---- test/audit-package.test.js | 152 +++++++++++++++++++++++++++++++++- 3 files changed, 275 insertions(+), 45 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d77a6db..7618b29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,38 +68,30 @@ jobs: - run: npm ci - run: npm test - - run: npm run package:audit - - name: Pack the exact release artifact - id: pack + - name: Pack, audit, and publish the exact release artifact run: | set -euo pipefail - npm pack --json > pack-result.json + artifact_dir="$(mktemp -d "$RUNNER_TEMP/agentcommunity-mcp-release.XXXXXX")" + trap 'rm -rf -- "$artifact_dir"' EXIT + pack_result="$artifact_dir/pack-result.json" + npm pack --json --pack-destination "$artifact_dir" > "$pack_result" tarball="$( - node --input-type=module <<'NODE' - import { readFileSync } from "node:fs"; - import { basename } from "node:path"; + PACK_RESULT="$pack_result" ARTIFACT_DIR="$artifact_dir" node --input-type=module <<'NODE' + import { existsSync, readFileSync } from "node:fs"; + import { join } from "node:path"; - const manifest = JSON.parse(readFileSync("package.json", "utf8")); - const results = JSON.parse(readFileSync("pack-result.json", "utf8")); - if (!Array.isArray(results) || results.length !== 1) { - throw new Error(`Expected one packed artifact, received ${results.length}`); - } + import { normalizePackResult } from "./scripts/audit-package.mjs"; - const [result] = results; - if (result.name !== manifest.name || result.version !== manifest.version) { - throw new Error("Packed artifact identity does not match package.json"); - } - if (typeof result.filename !== "string" || basename(result.filename) !== result.filename) { - throw new Error("Packed artifact filename is missing or unsafe"); - } + const manifest = JSON.parse(readFileSync("package.json", "utf8")); + const results = JSON.parse(readFileSync(process.env.PACK_RESULT, "utf8")); + const result = normalizePackResult(results, manifest.version); + const tarball = join(process.env.ARTIFACT_DIR, result.filename); + if (!existsSync(tarball)) throw new Error("Packed artifact is missing"); - process.stdout.write(result.filename); + process.stdout.write(tarball); NODE )" - test -f "$tarball" sha256sum "$tarball" - printf 'tarball=%s\n' "$tarball" >> "$GITHUB_OUTPUT" - - - name: Publish the exact artifact with OIDC provenance - run: npm publish "${{ steps.pack.outputs.tarball }}" --access public --provenance + npm run package:audit -- --tarball "$tarball" --pack-result "$pack_result" + npm publish "$tarball" --access public --provenance diff --git a/scripts/audit-package.mjs b/scripts/audit-package.mjs index 970e79b..6e9ebca 100644 --- a/scripts/audit-package.mjs +++ b/scripts/audit-package.mjs @@ -25,6 +25,10 @@ const expectedRepository = { }; const expectedEngines = { node: '>=20.18.1' }; const expectedDependencies = { 'mcp-remote': '0.1.38' }; +const expectedScripts = { + test: 'node --test', + 'package:audit': 'node scripts/audit-package.mjs', +}; const hostedMcpUrl = 'https://agentcommunity.org/mcp'; const brandedHeading = 'Official Agent Community MCP connector'; const secretPatterns = [ @@ -143,6 +147,44 @@ export function buildShimHelpInvocation( return { command: shimPath, args: ['--help'] }; } +export function parseAuditArguments(argv, cwd = process.cwd()) { + if (argv.length === 0) return { mode: 'pack' }; + + let tarball; + let packResult; + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index]; + const value = argv[index + 1]; + if (flag !== '--tarball' && flag !== '--pack-result') { + throw new Error(`Unknown package audit argument: ${flag ?? '(missing)'}.`); + } + if (typeof value !== 'string' || value.length === 0 || value.startsWith('--')) { + throw new Error(`Package audit argument ${flag} requires a path value.`); + } + if (flag === '--tarball') { + if (tarball !== undefined) throw new Error('Package audit argument --tarball was repeated.'); + tarball = value; + } else { + if (packResult !== undefined) { + throw new Error('Package audit argument --pack-result was repeated.'); + } + packResult = value; + } + } + + if (tarball === undefined || packResult === undefined) { + throw new Error('Explicit package audit requires both --tarball and --pack-result.'); + } + + const tarballPath = resolve(cwd, tarball); + const packResultPath = resolve(cwd, packResult); + if (tarballPath === packResultPath) { + throw new Error('Tarball and pack-result paths must be distinct.'); + } + + return { mode: 'provided', tarballPath, packResultPath }; +} + function command(commandName, args, cwd) { const result = spawnSync(commandName, args, { cwd, @@ -172,7 +214,7 @@ function invalidPackOutput(message) { throw new Error(`Invalid npm pack JSON output: ${message}.`); } -function normalizePackResult(value, expectedVersion) { +export function normalizePackResult(value, expectedVersion) { let candidate; if (Array.isArray(value)) { @@ -200,6 +242,12 @@ function normalizePackResult(value, expectedVersion) { ) { invalidPackOutput('tarball filename is not the expected package filename'); } + if (!/^[a-f0-9]{40}$/.test(candidate.shasum)) { + invalidPackOutput('result shasum is malformed'); + } + if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(candidate.integrity)) { + invalidPackOutput('result integrity is malformed'); + } if (!Array.isArray(candidate.files)) invalidPackOutput('result files are missing'); if (candidate.files.some(function (file) { return !isRecord(file) @@ -215,7 +263,7 @@ function normalizePackResult(value, expectedVersion) { return candidate; } -function assertExactManifest(manifest, expectedVersion, source) { +export function assertExactManifest(manifest, expectedVersion, source) { assert.equal(manifest.name, expectedName, `${source} package name drift detected`); assert.equal(manifest.version, expectedVersion, `${source} package version drift detected`); assert.deepEqual(manifest.bin, expectedBin, `${source} binary mapping drift detected`); @@ -234,18 +282,17 @@ function assertExactManifest(manifest, expectedVersion, source) { `${source} dependency boundary drift detected`, ); assert.deepEqual(manifest.exports, {}, `${source} exports must be an exact empty map`); + assert.deepEqual(manifest.scripts, expectedScripts, `${source} scripts map drift detected`); +} - for (const scriptName of ['preinstall', 'install', 'postinstall']) { - assert.equal( - manifest.scripts?.[scriptName], - undefined, - `${source} manifest must not contain a ${scriptName} script`, - ); - } +export function assertCompleteManifest(manifest, sourceManifest, source) { + assertExactManifest(manifest, sourceManifest.version, source); + assert.deepEqual(manifest, sourceManifest, `${source} complete manifest drift detected`); } -async function main() { +async function main(argv = process.argv.slice(2)) { const repositoryRoot = fileURLToPath(new URL('../', import.meta.url)); + const auditArguments = parseAuditArguments(argv); const sourceManifest = JSON.parse(await readFile(join(repositoryRoot, 'package.json'), 'utf8')); if (typeof sourceManifest.version !== 'string' || sourceManifest.version.length === 0) { throw new Error('Source package version is missing.'); @@ -256,22 +303,54 @@ async function main() { let installDirectory; try { - packDirectory = await mkdtemp(join(tmpdir(), 'agentcommunity-mcp-pack-')); installDirectory = await mkdtemp(join(tmpdir(), 'agentcommunity-mcp-install-')); - const packOutput = npmCommand( - ['pack', '--json', '--pack-destination', packDirectory], - repositoryRoot, - ); + let packOutput; + let tarballPath; + if (auditArguments.mode === 'pack') { + packDirectory = await mkdtemp(join(tmpdir(), 'agentcommunity-mcp-pack-')); + packOutput = npmCommand( + ['pack', '--json', '--pack-destination', packDirectory], + repositoryRoot, + ); + } else { + packOutput = await readFile(auditArguments.packResultPath, 'utf8'); + } + const packResult = normalizePackResult(JSON.parse(packOutput), sourceManifest.version); const inventory = packResult.files.map(function (file) { return file.path; }).sort(); assert.deepEqual(inventory, expectedFiles, 'Unexpected package inventory'); - const tarballPath = join(packDirectory, packResult.filename); + if (auditArguments.mode === 'pack') { + tarballPath = join(packDirectory, packResult.filename); + } else { + tarballPath = auditArguments.tarballPath; + assert.equal( + basename(tarballPath), + packResult.filename, + 'Provided tarball filename does not match pack-result JSON', + ); + assert.equal( + (await lstat(tarballPath)).isFile(), + true, + 'Provided tarball is not a regular file', + ); + } + + const tarInventory = command('tar', ['-tzf', tarballPath], repositoryRoot) + .split(/\r?\n/) + .filter(function (entry) { return entry.length > 0; }) + .map(function (entry) { + assert.ok(entry.startsWith('package/'), `Unexpected tarball entry root: ${entry}`); + return entry.slice('package/'.length); + }) + .sort(); + assert.deepEqual(tarInventory, expectedFiles, 'Unexpected tarball inventory'); + const packedManifest = JSON.parse( command('tar', ['-xOf', tarballPath, 'package/package.json'], repositoryRoot), ); - assertExactManifest(packedManifest, sourceManifest.version, 'Packed tarball'); + assertCompleteManifest(packedManifest, sourceManifest, 'Packed tarball'); for (const packedPath of inventory) { const content = command( @@ -287,6 +366,16 @@ async function main() { const tarball = await readFile(tarballPath); const tarballSha256 = createHash('sha256').update(tarball).digest('hex'); + assert.equal( + createHash('sha1').update(tarball).digest('hex'), + packResult.shasum, + 'Tarball SHA-1 does not match pack-result JSON', + ); + assert.equal( + `sha512-${createHash('sha512').update(tarball).digest('base64')}`, + packResult.integrity, + 'Tarball integrity does not match pack-result JSON', + ); await writeFile( join(installDirectory, 'package.json'), @@ -306,7 +395,7 @@ async function main() { const installedManifest = JSON.parse( await readFile(join(installedPackageRoot, 'package.json'), 'utf8'), ); - assertExactManifest(installedManifest, sourceManifest.version, 'Installed package'); + assertCompleteManifest(installedManifest, sourceManifest, 'Installed package'); const installedShim = join( installDirectory, @@ -331,6 +420,7 @@ async function main() { assert.ok(help.stdout.includes(hostedMcpUrl), 'Hosted MCP URL is missing from help'); process.stdout.write(`${JSON.stringify({ + artifact_source: auditArguments.mode === 'pack' ? 'packed' : 'provided', filename: packResult.filename, sha256: tarballSha256, files: inventory, diff --git a/test/audit-package.test.js b/test/audit-package.test.js index 59a72eb..4f9a74c 100644 --- a/test/audit-package.test.js +++ b/test/audit-package.test.js @@ -1,10 +1,38 @@ import assert from 'node:assert/strict'; -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { execFile } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { access, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { join, resolve } from 'node:path'; import test from 'node:test'; +import { promisify } from 'node:util'; +import { fileURLToPath } from 'node:url'; const auditModulePromise = import('../scripts/audit-package.mjs'); +const execFileAsync = promisify(execFile); +const packageRoot = fileURLToPath(new URL('../', import.meta.url)); +const auditScript = fileURLToPath(new URL('../scripts/audit-package.mjs', import.meta.url)); + +async function executeNpm(args, cwd) { + if (process.env.npm_execpath !== undefined) { + return execFileAsync(process.execPath, [process.env.npm_execpath, ...args], { cwd }); + } + if (process.platform === 'win32') { + const commandLine = ['npm', ...args] + .map(function (argument) { return `"${argument.replaceAll('"', '""')}"`; }) + .join(' '); + return execFileAsync( + process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe', + ['/d', '/s', '/c', commandLine], + { cwd }, + ); + } + return execFileAsync('npm', args, { cwd }); +} + +function firstPackResult(value) { + return Array.isArray(value) ? value[0] : Object.values(value)[0]; +} test('validates and invokes the regular npm cmd shim on Windows', async function () { const audit = await auditModulePromise; @@ -82,3 +110,123 @@ test('detects a JWT with the minimal valid header without matching invalid JSON' false, ); }); + +test('audits a caller-owned exact tarball without repacking or deleting it', async function () { + const artifactDirectory = await mkdtemp(join(tmpdir(), 'agentcommunity-mcp-exact-artifact-')); + const packResultPath = join(artifactDirectory, 'pack-result.json'); + + try { + const packed = await executeNpm( + ['pack', '--json', '--pack-destination', artifactDirectory], + packageRoot, + ); + await writeFile(packResultPath, packed.stdout); + const packResult = firstPackResult(JSON.parse(packed.stdout)); + const tarballPath = join(artifactDirectory, packResult.filename); + const beforeHash = createHash('sha256').update(await readFile(tarballPath)).digest('hex'); + + const audited = await execFileAsync( + process.execPath, + [auditScript, '--tarball', tarballPath, '--pack-result', packResultPath], + { cwd: packageRoot, maxBuffer: 16 * 1024 * 1024 }, + ); + const auditResult = JSON.parse(audited.stdout); + + assert.equal(auditResult.artifact_source, 'provided'); + assert.equal(auditResult.filename, packResult.filename); + assert.equal(auditResult.sha256, beforeHash); + await access(tarballPath); + await access(packResultPath); + assert.equal( + createHash('sha256').update(await readFile(tarballPath)).digest('hex'), + beforeHash, + ); + + await writeFile( + tarballPath, + Buffer.concat([await readFile(tarballPath), Buffer.from('tampered')]), + ); + await assert.rejects( + execFileAsync( + process.execPath, + [auditScript, '--tarball', tarballPath, '--pack-result', packResultPath], + { cwd: packageRoot, maxBuffer: 16 * 1024 * 1024 }, + ), + /Tarball SHA-1 does not match pack-result JSON/, + ); + await access(tarballPath); + await access(packResultPath); + } finally { + await rm(artifactDirectory, { recursive: true, force: true }); + } +}); + +test('requires a paired explicit artifact CLI contract', async function () { + const audit = await auditModulePromise; + assert.equal(typeof audit.parseAuditArguments, 'function'); + const fixtureWorkspace = resolve(packageRoot, 'fixture-workspace'); + + assert.deepEqual(audit.parseAuditArguments([], fixtureWorkspace), { mode: 'pack' }); + assert.deepEqual( + audit.parseAuditArguments( + ['--tarball', 'release.tgz', '--pack-result', 'pack-result.json'], + fixtureWorkspace, + ), + { + mode: 'provided', + tarballPath: join(fixtureWorkspace, 'release.tgz'), + packResultPath: join(fixtureWorkspace, 'pack-result.json'), + }, + ); + assert.throws( + function () { audit.parseAuditArguments(['--tarball', 'release.tgz'], fixtureWorkspace); }, + /requires both --tarball and --pack-result/, + ); + assert.throws( + function () { audit.parseAuditArguments(['--unknown', 'value'], fixtureWorkspace); }, + /Unknown package audit argument/, + ); +}); + +test('requires complete manifest equality and the exact scripts map', async function () { + const audit = await auditModulePromise; + assert.equal(typeof audit.assertExactManifest, 'function'); + assert.equal(typeof audit.assertCompleteManifest, 'function'); + + const sourceManifest = JSON.parse( + await readFile(new URL('../package.json', import.meta.url), 'utf8'), + ); + const lifecycleDrift = structuredClone(sourceManifest); + lifecycleDrift.scripts.prepare = 'node unexpected-prepare.js'; + assert.throws( + function () { + audit.assertExactManifest(lifecycleDrift, lifecycleDrift.version, 'Source'); + }, + /scripts map drift detected/, + ); + + const metadataDrift = structuredClone(sourceManifest); + metadataDrift.description = `${metadataDrift.description} changed`; + assert.throws( + function () { + audit.assertCompleteManifest(metadataDrift, sourceManifest, 'Packed tarball'); + }, + /complete manifest drift detected/, + ); +}); + +test('release workflow packs once then audits and publishes the same tarball path', async function () { + const workflow = await readFile( + new URL('../.github/workflows/release.yml', import.meta.url), + 'utf8', + ); + + assert.equal(workflow.match(/\bnpm pack\b/g)?.length, 1); + assert.match( + workflow, + /npm run package:audit -- --tarball "\$tarball" --pack-result "\$pack_result"/, + ); + assert.match(workflow, /npm publish "\$tarball" --access public --provenance/); + assert.ok(workflow.indexOf('npm pack ') < workflow.indexOf('npm run package:audit --')); + assert.ok(workflow.indexOf('npm run package:audit --') < workflow.indexOf('npm publish "$tarball"')); +}); From 70d75024fbfab809791dccbb050c0f203fb19895 Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 17:35:30 +0700 Subject: [PATCH 10/12] fix(ci): test connector on Windows --- .github/workflows/ci.yml | 7 ++- test/bin.test.js | 120 +++++++++++++++++++++++++++++++++++---- 2 files changed, 113 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9aadbc6..5b78871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,14 @@ permissions: jobs: test: - name: Node ${{ matrix.node }} - runs-on: ubuntu-24.04 + name: ${{ matrix.os }} / Node ${{ matrix.node }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + os: + - ubuntu-24.04 + - windows-2025 node: - "20.18.1" - "22" diff --git a/test/bin.test.js b/test/bin.test.js index c7e5752..dbb6261 100644 --- a/test/bin.test.js +++ b/test/bin.test.js @@ -12,7 +12,91 @@ import { applyChildExit, buildInvocation, helpText, installSignalForwarding } fr const execFileAsync = promisify(execFile); const packageRoot = fileURLToPath(new URL('../', import.meta.url)); -const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + +function quoteForComSpec(argument) { + return `"${argument.replaceAll('"', '""')}"`; +} + +function buildComSpecInvocation(executable, args, environment) { + const commandLine = [executable, ...args].map(quoteForComSpec).join(' '); + + return { + command: environment.ComSpec ?? environment.COMSPEC ?? 'cmd.exe', + args: ['/d', '/s', '/c', `"${commandLine}"`], + }; +} + +function buildNpmInvocation( + args, + platform = process.platform, + environment = process.env, + nodeExecPath = process.execPath, +) { + if (typeof environment.npm_execpath === 'string' && environment.npm_execpath.length > 0) { + return { command: nodeExecPath, args: [environment.npm_execpath, ...args] }; + } + if (platform === 'win32') { + return buildComSpecInvocation('npm.cmd', args, environment); + } + return { command: 'npm', args }; +} + +function buildInstalledShimInvocation( + shimPath, + platform = process.platform, + environment = process.env, +) { + return platform === 'win32' + ? buildComSpecInvocation(shimPath, ['--help'], environment) + : { command: shimPath, args: ['--help'] }; +} + +test('builds portable npm invocations for the npm CLI and Windows cmd fallback', function () { + const npmExecPath = 'C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js'; + const nodeExecPath = 'C:\\Program Files\\nodejs\\node.exe'; + const npmArgs = ['pack', '--pack-destination', 'C:\\Temp Root & Cache\\pack']; + + assert.deepEqual( + buildNpmInvocation(npmArgs, 'win32', { npm_execpath: npmExecPath }, nodeExecPath), + { + command: nodeExecPath, + args: [npmExecPath, ...npmArgs], + }, + ); + assert.deepEqual( + buildNpmInvocation(npmArgs, 'win32', { ComSpec: 'C:\\Windows\\System32\\cmd.exe' }, nodeExecPath), + { + command: 'C:\\Windows\\System32\\cmd.exe', + args: [ + '/d', + '/s', + '/c', + '""npm.cmd" "pack" "--pack-destination" "C:\\Temp Root & Cache\\pack""', + ], + }, + ); +}); + +test('builds the installed Windows shim invocation through ComSpec with robust quoting', function () { + const shimPath = 'C:\\Temp Root & Cache\\node_modules\\.bin\\agentcommunity-mcp.cmd'; + + assert.deepEqual( + buildInstalledShimInvocation(shimPath, 'win32', { COMSPEC: 'C:\\Windows\\cmd.exe' }), + { + command: 'C:\\Windows\\cmd.exe', + args: [ + '/d', + '/s', + '/c', + '""C:\\Temp Root & Cache\\node_modules\\.bin\\agentcommunity-mcp.cmd" "--help""', + ], + }, + ); + assert.deepEqual( + buildInstalledShimInvocation('/tmp/agentcommunity-mcp', 'linux', {}), + { command: '/tmp/agentcommunity-mcp', args: ['--help'] }, + ); +}); async function withPackedInstall(callback) { const temporaryRoot = await mkdtemp(join(tmpdir(), 'agentcommunity-mcp-')); @@ -24,17 +108,26 @@ async function withPackedInstall(callback) { await mkdir(clientDirectory); await writeFile(join(clientDirectory, 'package.json'), '{"private":true,"type":"module"}\n'); - const packed = await execFileAsync( - npmCommand, - ['pack', '--json', '--pack-destination', packDirectory], - { cwd: packageRoot }, - ); + const packInvocation = buildNpmInvocation([ + 'pack', + '--json', + '--pack-destination', + packDirectory, + ]); + const packed = await execFileAsync(packInvocation.command, packInvocation.args, { + cwd: packageRoot, + }); const [{ filename }] = JSON.parse(packed.stdout); - await execFileAsync( - npmCommand, - ['install', '--ignore-scripts', '--no-audit', '--no-fund', join(packDirectory, filename)], - { cwd: clientDirectory }, - ); + const installInvocation = buildNpmInvocation([ + 'install', + '--ignore-scripts', + '--no-audit', + '--no-fund', + join(packDirectory, filename), + ]); + await execFileAsync(installInvocation.command, installInvocation.args, { + cwd: clientDirectory, + }); await callback(clientDirectory); } finally { @@ -66,7 +159,10 @@ test('packed package emits help through the installed npm shim', async function ? 'agentcommunity-mcp.cmd' : 'agentcommunity-mcp'; const shimPath = join(clientDirectory, 'node_modules', '.bin', shimName); - const result = await execFileAsync(shimPath, ['--help'], { cwd: clientDirectory }); + const helpInvocation = buildInstalledShimInvocation(shimPath); + const result = await execFileAsync(helpInvocation.command, helpInvocation.args, { + cwd: clientDirectory, + }); assert.match(result.stdout, /Official Agent Community MCP connector/); assert.match(result.stdout, /https:\/\/agentcommunity\.org\/mcp/); From 8f68b9ce8c45306cf2a5162e66f83f7371ce3ce2 Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 17:44:44 +0700 Subject: [PATCH 11/12] fix(package): quote Windows audit commands --- scripts/audit-package.mjs | 41 +++++++++++++++++------- test/audit-package.test.js | 64 ++++++++++++++++++++++++++++---------- 2 files changed, 78 insertions(+), 27 deletions(-) diff --git a/scripts/audit-package.mjs b/scripts/audit-package.mjs index 6e9ebca..e801f1b 100644 --- a/scripts/audit-package.mjs +++ b/scripts/audit-package.mjs @@ -132,17 +132,39 @@ export async function validateInstalledShimTarget( return resolvedShim; } +function quoteCmdArgument(argument) { + if (argument.includes('"')) { + throw new Error('Windows command arguments must not contain double quotes.'); + } + return `"${argument}"`; +} + +export function buildComSpecInvocation(executable, args, environment = process.env) { + const commandLine = `"${[executable, ...args].map(quoteCmdArgument).join(' ')}"`; + return { + command: environment.ComSpec ?? environment.COMSPEC ?? 'cmd.exe', + args: ['/d', '/s', '/c', commandLine], + }; +} + +export function buildNpmInvocation( + args, + platform = process.platform, + environment = process.env, +) { + if (typeof environment.npm_execpath === 'string' && environment.npm_execpath.length > 0) { + return { command: process.execPath, args: [environment.npm_execpath, ...args] }; + } + if (platform === 'win32') return buildComSpecInvocation('npm.cmd', args, environment); + return { command: 'npm', args }; +} + export function buildShimHelpInvocation( shimPath, platform = process.platform, environment = process.env, ) { - if (platform === 'win32') { - return { - command: environment.ComSpec ?? environment.COMSPEC ?? 'cmd.exe', - args: ['/d', '/s', '/c', `"${shimPath}" --help`], - }; - } + if (platform === 'win32') return buildComSpecInvocation(shimPath, ['--help'], environment); return { command: shimPath, args: ['--help'] }; } @@ -203,11 +225,8 @@ function command(commandName, args, cwd) { } function npmCommand(args, cwd) { - const npmExecPath = process.env.npm_execpath; - - return npmExecPath === undefined - ? command(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, cwd) - : command(process.execPath, [npmExecPath, ...args], cwd); + const invocation = buildNpmInvocation(args); + return command(invocation.command, invocation.args, cwd); } function invalidPackOutput(message) { diff --git a/test/audit-package.test.js b/test/audit-package.test.js index 4f9a74c..a478d1f 100644 --- a/test/audit-package.test.js +++ b/test/audit-package.test.js @@ -14,20 +14,9 @@ const packageRoot = fileURLToPath(new URL('../', import.meta.url)); const auditScript = fileURLToPath(new URL('../scripts/audit-package.mjs', import.meta.url)); async function executeNpm(args, cwd) { - if (process.env.npm_execpath !== undefined) { - return execFileAsync(process.execPath, [process.env.npm_execpath, ...args], { cwd }); - } - if (process.platform === 'win32') { - const commandLine = ['npm', ...args] - .map(function (argument) { return `"${argument.replaceAll('"', '""')}"`; }) - .join(' '); - return execFileAsync( - process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe', - ['/d', '/s', '/c', commandLine], - { cwd }, - ); - } - return execFileAsync('npm', args, { cwd }); + const audit = await auditModulePromise; + const invocation = audit.buildNpmInvocation(args); + return execFileAsync(invocation.command, invocation.args, { cwd }); } function firstPackResult(value) { @@ -38,6 +27,8 @@ test('validates and invokes the regular npm cmd shim on Windows', async function const audit = await auditModulePromise; assert.equal(typeof audit.installedShimName, 'function'); assert.equal(typeof audit.validateInstalledShimTarget, 'function'); + assert.equal(typeof audit.buildComSpecInvocation, 'function'); + assert.equal(typeof audit.buildNpmInvocation, 'function'); assert.equal(typeof audit.buildShimHelpInvocation, 'function'); const temporaryRoot = await mkdtemp(join(tmpdir(), 'agentcommunity-mcp-windows-shim-')); @@ -61,11 +52,52 @@ test('validates and invokes the regular npm cmd shim on Windows', async function await audit.validateInstalledShimTarget(shimPath, targetPath, 'win32'), await realpath(targetPath), ); + const spacedShimPath = 'C:\\Temporary Folder\\agentcommunity-mcp.cmd'; + const expectedCmdInvocation = { + command: 'C:\\Windows\\cmd.exe', + args: [ + '/d', + '/s', + '/c', + '""C:\\Temporary Folder\\agentcommunity-mcp.cmd" "--help""', + ], + }; + assert.deepEqual( + audit.buildComSpecInvocation( + spacedShimPath, + ['--help'], + { ComSpec: 'C:\\Windows\\cmd.exe' }, + ), + expectedCmdInvocation, + ); + assert.deepEqual( + audit.buildShimHelpInvocation( + spacedShimPath, + 'win32', + { ComSpec: 'C:\\Windows\\cmd.exe' }, + ), + expectedCmdInvocation, + ); assert.deepEqual( - audit.buildShimHelpInvocation(shimPath, 'win32', { ComSpec: 'C:\\Windows\\cmd.exe' }), + audit.buildNpmInvocation( + ['pack', '--json'], + 'win32', + { ComSpec: 'C:\\Windows\\cmd.exe' }, + ), { command: 'C:\\Windows\\cmd.exe', - args: ['/d', '/s', '/c', `"${shimPath}" --help`], + args: ['/d', '/s', '/c', '""npm.cmd" "pack" "--json""'], + }, + ); + assert.deepEqual( + audit.buildNpmInvocation( + ['install'], + 'win32', + { npm_execpath: 'C:\\Program Files\\npm-cli.js' }, + ), + { + command: process.execPath, + args: ['C:\\Program Files\\npm-cli.js', 'install'], }, ); assert.deepEqual( From 65a598dddad2e1037b870ecacbabf35aec72110f Mon Sep 17 00:00:00 2001 From: nembal Date: Mon, 17 Aug 2026 17:58:37 +0700 Subject: [PATCH 12/12] fix(ci): make package audit portable --- scripts/audit-package.mjs | 33 +++++++++++++++------------- test/audit-package.test.js | 45 +++++++++++++++++++++++++++++++++++++- test/bin.test.js | 6 +++++ 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/scripts/audit-package.mjs b/scripts/audit-package.mjs index e801f1b..fbd84c7 100644 --- a/scripts/audit-package.mjs +++ b/scripts/audit-package.mjs @@ -144,6 +144,7 @@ export function buildComSpecInvocation(executable, args, environment = process.e return { command: environment.ComSpec ?? environment.COMSPEC ?? 'cmd.exe', args: ['/d', '/s', '/c', commandLine], + options: { windowsVerbatimArguments: true }, }; } @@ -207,9 +208,10 @@ export function parseAuditArguments(argv, cwd = process.cwd()) { return { mode: 'provided', tarballPath, packResultPath }; } -function command(commandName, args, cwd) { +function command(commandName, args, cwd, options = {}) { const result = spawnSync(commandName, args, { cwd, + ...options, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, }); @@ -226,7 +228,7 @@ function command(commandName, args, cwd) { function npmCommand(args, cwd) { const invocation = buildNpmInvocation(args); - return command(invocation.command, invocation.args, cwd); + return command(invocation.command, invocation.args, cwd, invocation.options); } function invalidPackOutput(message) { @@ -356,6 +358,19 @@ async function main(argv = process.argv.slice(2)) { ); } + const tarball = await readFile(tarballPath); + const tarballSha256 = createHash('sha256').update(tarball).digest('hex'); + assert.equal( + createHash('sha1').update(tarball).digest('hex'), + packResult.shasum, + 'Tarball SHA-1 does not match pack-result JSON', + ); + assert.equal( + `sha512-${createHash('sha512').update(tarball).digest('base64')}`, + packResult.integrity, + 'Tarball integrity does not match pack-result JSON', + ); + const tarInventory = command('tar', ['-tzf', tarballPath], repositoryRoot) .split(/\r?\n/) .filter(function (entry) { return entry.length > 0; }) @@ -383,19 +398,6 @@ async function main(argv = process.argv.slice(2)) { if (containsJwt(content)) throw new Error(`Possible JWT in packed file ${packedPath}.`); } - const tarball = await readFile(tarballPath); - const tarballSha256 = createHash('sha256').update(tarball).digest('hex'); - assert.equal( - createHash('sha1').update(tarball).digest('hex'), - packResult.shasum, - 'Tarball SHA-1 does not match pack-result JSON', - ); - assert.equal( - `sha512-${createHash('sha512').update(tarball).digest('base64')}`, - packResult.integrity, - 'Tarball integrity does not match pack-result JSON', - ); - await writeFile( join(installDirectory, 'package.json'), '{"name":"agentcommunity-mcp-clean-install","private":true,"version":"1.0.0"}\n', @@ -430,6 +432,7 @@ async function main(argv = process.argv.slice(2)) { const helpInvocation = buildShimHelpInvocation(installedShim); const help = spawnSync(helpInvocation.command, helpInvocation.args, { cwd: installDirectory, + ...helpInvocation.options, encoding: 'utf8', maxBuffer: 1024 * 1024, }); diff --git a/test/audit-package.test.js b/test/audit-package.test.js index a478d1f..eb7d151 100644 --- a/test/audit-package.test.js +++ b/test/audit-package.test.js @@ -16,7 +16,7 @@ const auditScript = fileURLToPath(new URL('../scripts/audit-package.mjs', import async function executeNpm(args, cwd) { const audit = await auditModulePromise; const invocation = audit.buildNpmInvocation(args); - return execFileAsync(invocation.command, invocation.args, { cwd }); + return execFileAsync(invocation.command, invocation.args, { cwd, ...invocation.options }); } function firstPackResult(value) { @@ -61,6 +61,7 @@ test('validates and invokes the regular npm cmd shim on Windows', async function '/c', '""C:\\Temporary Folder\\agentcommunity-mcp.cmd" "--help""', ], + options: { windowsVerbatimArguments: true }, }; assert.deepEqual( audit.buildComSpecInvocation( @@ -87,6 +88,7 @@ test('validates and invokes the regular npm cmd shim on Windows', async function { command: 'C:\\Windows\\cmd.exe', args: ['/d', '/s', '/c', '""npm.cmd" "pack" "--json""'], + options: { windowsVerbatimArguments: true }, }, ); assert.deepEqual( @@ -127,6 +129,47 @@ test('validates and invokes the regular npm cmd shim on Windows', async function } }); +test('rejects mismatched bytes before attempting to parse a corrupt tarball', async function () { + const artifactDirectory = await mkdtemp(join(tmpdir(), 'agentcommunity-mcp-corrupt-artifact-')); + const sourceManifest = JSON.parse( + await readFile(new URL('../package.json', import.meta.url), 'utf8'), + ); + const filename = `agentcommunity-mcp-${sourceManifest.version}.tgz`; + const tarballPath = join(artifactDirectory, filename); + const packResultPath = join(artifactDirectory, 'pack-result.json'); + const expectedBytes = Buffer.from('different artifact bytes'); + const packResult = [{ + name: sourceManifest.name, + version: sourceManifest.version, + filename, + shasum: createHash('sha1').update(expectedBytes).digest('hex'), + integrity: `sha512-${createHash('sha512').update(expectedBytes).digest('base64')}`, + files: [ + { path: 'LICENSE', size: 1 }, + { path: 'README.md', size: 1 }, + { path: 'SECURITY.md', size: 1 }, + { path: 'bin.js', size: 1 }, + { path: 'package.json', size: 1 }, + ], + }]; + + try { + await writeFile(tarballPath, 'not a tarball'); + await writeFile(packResultPath, JSON.stringify(packResult)); + + await assert.rejects( + execFileAsync( + process.execPath, + [auditScript, '--tarball', tarballPath, '--pack-result', packResultPath], + { cwd: packageRoot, maxBuffer: 16 * 1024 * 1024 }, + ), + /Tarball SHA-1 does not match pack-result JSON/, + ); + } finally { + await rm(artifactDirectory, { recursive: true, force: true }); + } +}); + test('detects a JWT with the minimal valid header without matching invalid JSON', async function () { const audit = await auditModulePromise; assert.equal(typeof audit.containsJwt, 'function'); diff --git a/test/bin.test.js b/test/bin.test.js index dbb6261..814094d 100644 --- a/test/bin.test.js +++ b/test/bin.test.js @@ -23,6 +23,7 @@ function buildComSpecInvocation(executable, args, environment) { return { command: environment.ComSpec ?? environment.COMSPEC ?? 'cmd.exe', args: ['/d', '/s', '/c', `"${commandLine}"`], + options: { windowsVerbatimArguments: true }, }; } @@ -73,6 +74,7 @@ test('builds portable npm invocations for the npm CLI and Windows cmd fallback', '/c', '""npm.cmd" "pack" "--pack-destination" "C:\\Temp Root & Cache\\pack""', ], + options: { windowsVerbatimArguments: true }, }, ); }); @@ -90,6 +92,7 @@ test('builds the installed Windows shim invocation through ComSpec with robust q '/c', '""C:\\Temp Root & Cache\\node_modules\\.bin\\agentcommunity-mcp.cmd" "--help""', ], + options: { windowsVerbatimArguments: true }, }, ); assert.deepEqual( @@ -116,6 +119,7 @@ async function withPackedInstall(callback) { ]); const packed = await execFileAsync(packInvocation.command, packInvocation.args, { cwd: packageRoot, + ...packInvocation.options, }); const [{ filename }] = JSON.parse(packed.stdout); const installInvocation = buildNpmInvocation([ @@ -127,6 +131,7 @@ async function withPackedInstall(callback) { ]); await execFileAsync(installInvocation.command, installInvocation.args, { cwd: clientDirectory, + ...installInvocation.options, }); await callback(clientDirectory); @@ -162,6 +167,7 @@ test('packed package emits help through the installed npm shim', async function const helpInvocation = buildInstalledShimInvocation(shimPath); const result = await execFileAsync(helpInvocation.command, helpInvocation.args, { cwd: clientDirectory, + ...helpInvocation.options, }); assert.match(result.stdout, /Official Agent Community MCP connector/);