diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 81b0691..b3b741d 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,19 +1,17 @@ -name: ESlint Test +name: ESLint + on: pull_request: branches: '**' jobs: - build: + lint: runs-on: ubuntu-latest - strategy: - matrix: - node: [20] steps: - uses: actions/checkout@v4 - - name: Setup Node 20 + - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 20 - - run: npm install + node-version: 24 + - run: npm ci - run: npm run lint diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c1faab9..c08a557 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,23 +1,22 @@ name: NPM Publish + on: release: types: [created] jobs: - build: + publish: runs-on: ubuntu-latest - strategy: - matrix: - node: [20] + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 - - name: Setup Node 20 + - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build - - run: npm publish - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + - run: npm publish --provenance --access public diff --git a/.gitignore b/.gitignore index e94116c..6b08be5 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,9 @@ lerna-debug.log # System Files .DS_Store Thumbs.db + +# Node Version Managers +.tool-versions + +# AI Agents or Tools +CLAUDE.md diff --git a/package-lock.json b/package-lock.json index 9d47927..1b7d206 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "phoenixd-mcp-server", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "phoenixd-mcp-server", - "version": "1.0.2", + "version": "1.1.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.2", - "zod": "^3.24.4" + "@modelcontextprotocol/sdk": "^1.25.3", + "zod": "^4.0.0" }, "bin": { "phoenixd-mcp-server": "build/index.js" @@ -22,6 +22,9 @@ "globals": "^16.1.0", "typescript": "^5.8.3", "typescript-eslint": "^8.32.1" + }, + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -165,19 +168,44 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -245,27 +273,66 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz", - "integrity": "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==", + "version": "1.25.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", + "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==", "license": "MIT", "dependencies": { - "ajv": "^6.12.6", + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -477,9 +544,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -584,6 +651,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -596,6 +664,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -627,29 +734,33 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -829,9 +940,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1221,6 +1332,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -1230,6 +1342,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -1464,32 +1592,50 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "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.1", - "toidentifier": "1.0.1" + "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.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -1589,10 +1735,19 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1613,8 +1768,15 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -1970,15 +2132,16 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -2021,18 +2184,27 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, "node_modules/resolve-from": { @@ -2272,9 +2444,9 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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" @@ -2425,6 +2597,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -2484,21 +2657,21 @@ } }, "node_modules/zod": { - "version": "3.25.23", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.23.tgz", - "integrity": "sha512-Od2bdMosahjSrSgJtakrwjMDb1zM1A3VIHCPGveZt/3/wlrTWBya2lmEh2OYe4OIu8mPTmmr0gnLHIWQXdtWBg==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "license": "ISC", "peerDependencies": { - "zod": "^3.24.1" + "zod": "^3.25 || ^4" } } } diff --git a/package.json b/package.json index 6ff4ef1..c8767a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "phoenixd-mcp-server", - "version": "1.0.2", + "version": "1.1.0", "description": "Connect a phoenixd bitcoin lightning wallet to your LLM.", "keywords": [ "phoenixd", @@ -36,9 +36,12 @@ "prepack": "npm run build", "start": "node build/index.js" }, + "engines": { + "node": ">=18" + }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.2", - "zod": "^3.24.4" + "@modelcontextprotocol/sdk": "^1.25.3", + "zod": "^4.0.0" }, "devDependencies": { "@eslint/js": "^9.26.0", diff --git a/src/index.ts b/src/index.ts index 2dda57c..12b2cf0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ const config = { const server = new McpServer({ name: 'phoenixd-mcp-server', - version: '1.0.1', + version: '1.1.0', }); await tools.registerGetBalanceTool(server, config); diff --git a/src/tools/bump_fee.ts b/src/tools/bump_fee.ts index 14aba5e..4570009 100644 --- a/src/tools/bump_fee.ts +++ b/src/tools/bump_fee.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerBumpFeeTool( server: McpServer, @@ -15,41 +16,22 @@ export function registerBumpFeeTool( }, async ({ feerateSatByte }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); + const params = new URLSearchParams({ feerateSatByte: feerateSatByte.toString(), }); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/bumpfee`, { + const result = await fetchPhoenixd(config, '/bumpfee', { method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${credentials}`, - }, - body: params.toString(), + body: params, + responseType: 'text', }); - const bumpFeeData = await data.text(); - - if (bumpFeeData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Bump fee failed', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: bumpFeeData, - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/close_channel.ts b/src/tools/close_channel.ts index 630d024..7a5c3ba 100644 --- a/src/tools/close_channel.ts +++ b/src/tools/close_channel.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerCloseChannelTool( server: McpServer, @@ -17,43 +18,24 @@ export function registerCloseChannelTool( }, async ({ channelId, address, feerateSatByte }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); + const params = new URLSearchParams({ channelId, address, feerateSatByte: feerateSatByte.toString(), }); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/closechannel`, { + const result = await fetchPhoenixd(config, '/closechannel', { method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${credentials}`, - }, - body: params.toString(), + body: params, + responseType: 'text', }); - const closeChannelData = await data.text(); - - if (closeChannelData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Channel not found', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: closeChannelData, - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/create_invoice.ts b/src/tools/create_invoice.ts index a9d9221..218e34a 100644 --- a/src/tools/create_invoice.ts +++ b/src/tools/create_invoice.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerCreateInvoiceTool( server: McpServer, @@ -20,48 +21,23 @@ export function registerCreateInvoiceTool( }, async ({ description, amountSat, expirySeconds, externalId, webhookUrl }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const paramsObj: Record = { - description, - }; + const paramsObj: Record = { description }; if (amountSat !== undefined) paramsObj.amountSat = amountSat.toString(); if (expirySeconds !== undefined) paramsObj.expirySeconds = expirySeconds.toString(); if (externalId !== undefined) paramsObj.externalId = externalId; if (webhookUrl !== undefined) paramsObj.webhookUrl = webhookUrl; - const params = new URLSearchParams(paramsObj); - - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/createinvoice`, { + const result = await fetchPhoenixd(config, '/createinvoice', { method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${credentials}`, - }, - body: params.toString(), + body: new URLSearchParams(paramsObj), }); - const createInvoiceData = await data.json(); - - if (createInvoiceData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Invoice not created', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(createInvoiceData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/create_offer.ts b/src/tools/create_offer.ts index 66dd324..7dfd9ac 100644 --- a/src/tools/create_offer.ts +++ b/src/tools/create_offer.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerCreateOfferTool( server: McpServer, @@ -16,44 +17,22 @@ export function registerCreateOfferTool( }, async ({ description, amountSat }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const paramsObj: Record = {}; + const paramsObj: Record = {}; if (description !== undefined) paramsObj.description = description; if (amountSat !== undefined) paramsObj.amountSat = amountSat.toString(); - const params = new URLSearchParams(paramsObj); - - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/createoffer`, { + const result = await fetchPhoenixd(config, '/createoffer', { method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${credentials}`, - }, - body: params.toString(), + body: new URLSearchParams(paramsObj), + responseType: 'text', }); - const createOfferData = await data.text(); - - if (createOfferData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Offer not created', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: createOfferData, - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/decode_invoice.ts b/src/tools/decode_invoice.ts index 0c31c71..9243db2 100644 --- a/src/tools/decode_invoice.ts +++ b/src/tools/decode_invoice.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerDecodeInvoiceTool( server: McpServer, @@ -15,41 +16,19 @@ export function registerDecodeInvoiceTool( }, async ({ invoice }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const params = new URLSearchParams({ - invoice, - }); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/decodeinvoice`, { + const params = new URLSearchParams({ invoice }); + + const result = await fetchPhoenixd(config, '/decodeinvoice', { method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${credentials}`, - }, - body: params.toString(), + body: params, }); - const decodedInvoiceData = await data.json(); - - if (decodedInvoiceData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Invoice not decoded', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(decodedInvoiceData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/decode_offer.ts b/src/tools/decode_offer.ts index b2d826d..9cee43b 100644 --- a/src/tools/decode_offer.ts +++ b/src/tools/decode_offer.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerDecodeOfferTool( server: McpServer, @@ -15,41 +16,19 @@ export function registerDecodeOfferTool( }, async ({ offer }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const params = new URLSearchParams({ - offer, - }); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/decodeoffer`, { + const params = new URLSearchParams({ offer }); + + const result = await fetchPhoenixd(config, '/decodeoffer', { method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${credentials}`, - }, - body: params.toString(), + body: params, }); - const decodedOfferData = await data.json(); - - if (decodedOfferData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Offer not decoded', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(decodedOfferData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/get_balance.ts b/src/tools/get_balance.ts index 31ff174..1671c9a 100644 --- a/src/tools/get_balance.ts +++ b/src/tools/get_balance.ts @@ -1,6 +1,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerGetBalanceTool( server: McpServer, @@ -11,36 +12,14 @@ export function registerGetBalanceTool( 'Get the balance of the node', async () => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/getbalance`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Basic ${credentials}`, - }, - }); - const balanceData = await data.json(); + const result = await fetchPhoenixd(config, '/getbalance', { method: 'GET' }); - if (balanceData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Balance not found', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(balanceData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/get_incoming_payment.ts b/src/tools/get_incoming_payment.ts index db0c1a1..f886853 100644 --- a/src/tools/get_incoming_payment.ts +++ b/src/tools/get_incoming_payment.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerGetIncomingPaymentTool( server: McpServer, @@ -15,37 +16,16 @@ export function registerGetIncomingPaymentTool( }, async ({ paymentHash }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/payments/incoming/${paymentHash}`, { + const result = await fetchPhoenixd(config, `/payments/incoming/${paymentHash}`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Basic ${credentials}`, - }, }); - const getIncomingPaymentData = await data.json(); - - if (getIncomingPaymentData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Incoming payment not found', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(getIncomingPaymentData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/get_node_info.ts b/src/tools/get_node_info.ts index 77a5908..0ca44ea 100644 --- a/src/tools/get_node_info.ts +++ b/src/tools/get_node_info.ts @@ -1,6 +1,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerGetNodeInfoTool( server: McpServer, @@ -11,36 +12,14 @@ export function registerGetNodeInfoTool( 'Get the node info', async () => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/getinfo`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Basic ${credentials}`, - }, - }); - const nodeInfoData = await data.json(); + const result = await fetchPhoenixd(config, '/getinfo', { method: 'GET' }); - if (nodeInfoData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Node info not found', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(nodeInfoData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/get_outgoing_payment.ts b/src/tools/get_outgoing_payment.ts index 0c21ce7..5bc9570 100644 --- a/src/tools/get_outgoing_payment.ts +++ b/src/tools/get_outgoing_payment.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerGetOutgoingPaymentTool( server: McpServer, @@ -15,37 +16,16 @@ export function registerGetOutgoingPaymentTool( }, async ({ paymentId }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/payments/outgoing/${paymentId}`, { + const result = await fetchPhoenixd(config, `/payments/outgoing/${paymentId}`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Basic ${credentials}`, - }, }); - const getOutgoingPaymentData = await data.json(); - - if (getOutgoingPaymentData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Incoming payment not found', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(getOutgoingPaymentData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/list_channels.ts b/src/tools/list_channels.ts index 2462eea..8264c87 100644 --- a/src/tools/list_channels.ts +++ b/src/tools/list_channels.ts @@ -1,6 +1,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerListChannelsTool( server: McpServer, @@ -11,36 +12,14 @@ export function registerListChannelsTool( 'List the node channels', async () => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/listchannels`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Basic ${credentials}`, - }, - }); - const channelsListData = await data.json(); + const result = await fetchPhoenixd(config, '/listchannels', { method: 'GET' }); - if (channelsListData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Channels not found', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(channelsListData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/list_incoming_payments.ts b/src/tools/list_incoming_payments.ts index d2f4a60..d854b88 100644 --- a/src/tools/list_incoming_payments.ts +++ b/src/tools/list_incoming_payments.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerListIncomingPaymentsTool( server: McpServer, @@ -20,55 +21,27 @@ export function registerListIncomingPaymentsTool( }, async ({ from, to, limit, offset, all, externalId }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); const paramsObj: Record = { - from: '0', - to: Date.now().toString(), - limit: '20', - offset: '0', - all: 'false', - externalId: '', + from: from ?? '0', + to: to ?? Date.now().toString(), + limit: limit?.toString() ?? '20', + offset: offset?.toString() ?? '0', + all: all ? 'true' : 'false', }; - - if (from !== undefined) paramsObj.from = from.toString(); - if (to !== undefined) paramsObj.to = to.toString(); - if (limit !== undefined) paramsObj.limit = limit.toString(); - if (offset !== undefined) paramsObj.offset = offset.toString(); - if (all !== undefined) paramsObj.all = all ? 'true' : 'false'; if (externalId !== undefined) paramsObj.externalId = externalId; const params = new URLSearchParams(paramsObj); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/payments/incoming?${params.toString()}`, { + const result = await fetchPhoenixd(config, `/payments/incoming?${params.toString()}`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Basic ${credentials}`, - }, }); - const listIncomingPaymentsData = await data.json(); - - if (listIncomingPaymentsData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Incoming payments not found', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(listIncomingPaymentsData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/list_outgoing_payments.ts b/src/tools/list_outgoing_payments.ts index 2999398..6f7f866 100644 --- a/src/tools/list_outgoing_payments.ts +++ b/src/tools/list_outgoing_payments.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerListOutgoingPaymentsTool( server: McpServer, @@ -19,53 +20,26 @@ export function registerListOutgoingPaymentsTool( }, async ({ from, to, limit, offset, all }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); const paramsObj: Record = { - from: '0', - to: Date.now().toString(), - limit: '20', - offset: '0', - all: 'false', + from: from ?? '0', + to: to ?? Date.now().toString(), + limit: limit?.toString() ?? '20', + offset: offset?.toString() ?? '0', + all: all ? 'true' : 'false', }; - if (from !== undefined) paramsObj.from = from.toString(); - if (to !== undefined) paramsObj.to = to.toString(); - if (limit !== undefined) paramsObj.limit = limit.toString(); - if (offset !== undefined) paramsObj.offset = offset.toString(); - if (all !== undefined) paramsObj.all = all ? 'true' : 'false'; - const params = new URLSearchParams(paramsObj); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/payments/outgoing?${params.toString()}`, { + const result = await fetchPhoenixd(config, `/payments/outgoing?${params.toString()}`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Basic ${credentials}`, - }, }); - const listOutgoingPaymentsData = await data.json(); - - if (listOutgoingPaymentsData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Outgoing payments not found', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(listOutgoingPaymentsData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/pay_invoice.ts b/src/tools/pay_invoice.ts index ce0c294..8392438 100644 --- a/src/tools/pay_invoice.ts +++ b/src/tools/pay_invoice.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerPayInvoiceTool( server: McpServer, @@ -16,45 +17,20 @@ export function registerPayInvoiceTool( }, async ({ amountSat, invoice }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const paramsObj: Record = { - invoice, - }; + const paramsObj: Record = { invoice }; if (amountSat !== undefined) paramsObj.amountSat = amountSat.toString(); - const params = new URLSearchParams(paramsObj); - - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/payinvoice`, { + const result = await fetchPhoenixd(config, '/payinvoice', { method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${credentials}`, - }, - body: params.toString(), + body: new URLSearchParams(paramsObj), }); - const payInvoiceData = await data.json(); - - if (payInvoiceData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Payment failed', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(payInvoiceData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/pay_lightning_address.ts b/src/tools/pay_lightning_address.ts index 26b642a..5ffddd3 100644 --- a/src/tools/pay_lightning_address.ts +++ b/src/tools/pay_lightning_address.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerPayLightningAddressTool( server: McpServer, @@ -17,46 +18,21 @@ export function registerPayLightningAddressTool( }, async ({ amountSat, address, message }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const paramsObj: Record = { - address, - }; + const paramsObj: Record = { address }; if (amountSat !== undefined) paramsObj.amountSat = amountSat.toString(); if (message !== undefined) paramsObj.message = message; - const params = new URLSearchParams(paramsObj); - - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/paylnaddress`, { + const result = await fetchPhoenixd(config, '/paylnaddress', { method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${credentials}`, - }, - body: params.toString(), + body: new URLSearchParams(paramsObj), }); - const payLightningAddressData = await data.json(); - - if (payLightningAddressData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Payment failed', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(payLightningAddressData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/pay_offer.ts b/src/tools/pay_offer.ts index 9896c54..5485084 100644 --- a/src/tools/pay_offer.ts +++ b/src/tools/pay_offer.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerPayOfferTool( server: McpServer, @@ -17,46 +18,21 @@ export function registerPayOfferTool( }, async ({ amountSat, offer, message }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); - const paramsObj: Record = { - offer, - }; + const paramsObj: Record = { offer }; if (amountSat !== undefined) paramsObj.amountSat = amountSat.toString(); if (message !== undefined) paramsObj.message = message; - const params = new URLSearchParams(paramsObj); - - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/payoffer`, { + const result = await fetchPhoenixd(config, '/payoffer', { method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${credentials}`, - }, - body: params.toString(), + body: new URLSearchParams(paramsObj), }); - const payOfferData = await data.json(); - - if (payOfferData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Payment failed', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: JSON.stringify(payOfferData, null, 2), - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/tools/pay_on_chain.ts b/src/tools/pay_on_chain.ts index d818694..d7d47da 100644 --- a/src/tools/pay_on_chain.ts +++ b/src/tools/pay_on_chain.ts @@ -1,7 +1,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { PhoenixdMcpConfig } from '../types'; +import { PhoenixdMcpConfig } from '../types/index.js'; import { validateEnv } from '../utils/validate_env.js'; +import { fetchPhoenixd, formatToolResponse, formatToolError } from '../utils/fetch_phoenixd.js'; export function registerPayOnChainTool( server: McpServer, @@ -17,43 +18,24 @@ export function registerPayOnChainTool( }, async ({ amountSat, address, feerateSatByte }) => { validateEnv(config); - const credentials = btoa(`:${config.httpPassword}`); + const params = new URLSearchParams({ amountSat: amountSat.toString(), address, feerateSatByte: feerateSatByte.toString(), }); - const data = await fetch(`${config.httpProtocol}://${config.httpHost}:${config.httpPort}/sendtoaddress`, { + const result = await fetchPhoenixd(config, '/sendtoaddress', { method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${credentials}`, - }, - body: params.toString(), + body: params, + responseType: 'text', }); - const payOnChainData = await data.text(); - - if (payOnChainData.length === 0) { - return { - content: [ - { - type: 'text', - text: 'Payment failed', - }, - ], - }; + if (!result.ok) { + return formatToolError(result.error); } - return { - content: [ - { - type: 'text', - text: payOnChainData, - }, - ], - }; + return formatToolResponse(result.data); }, ); } diff --git a/src/utils/fetch_phoenixd.ts b/src/utils/fetch_phoenixd.ts new file mode 100644 index 0000000..00968d0 --- /dev/null +++ b/src/utils/fetch_phoenixd.ts @@ -0,0 +1,74 @@ +import { PhoenixdMcpConfig } from '../types/index.js'; + +export type FetchResult = + | { ok: true; data: T } + | { ok: false; error: string }; + +export async function fetchPhoenixd( + config: PhoenixdMcpConfig, + endpoint: string, + options: { + method: 'GET' | 'POST'; + body?: URLSearchParams; + responseType?: 'json' | 'text'; + }, +): Promise> { + const credentials = btoa(`:${config.httpPassword}`); + const { method, body, responseType = 'json' } = options; + + try { + const response = await fetch( + `${config.httpProtocol}://${config.httpHost}:${config.httpPort}${endpoint}`, + { + method, + headers: { + 'Content-Type': method === 'POST' + ? 'application/x-www-form-urlencoded' + : 'application/json', + 'Authorization': `Basic ${credentials}`, + }, + body: body?.toString(), + }, + ); + + if (!response.ok) { + const errorText = await response.text().catch(() => response.statusText); + return { + ok: false, + error: `HTTP ${response.status}: ${errorText || response.statusText}`, + }; + } + + const data = responseType === 'text' + ? await response.text() + : await response.json(); + + return { ok: true, data: data as T }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown network error'; + return { ok: false, error: `Network error: ${message}` }; + } +} + +export function formatToolResponse(data: unknown) { + return { + content: [ + { + type: 'text' as const, + text: typeof data === 'string' ? data : JSON.stringify(data, null, 2), + }, + ], + }; +} + +export function formatToolError(error: string) { + return { + content: [ + { + type: 'text' as const, + text: error, + }, + ], + isError: true, + }; +}