diff --git a/.env.example b/.env.example index a4ddc65..4485fff 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,13 @@ # Rename this file to ".env" and fill in your secrets +# Port to run the server on +PORT=3001 + +# Set to true if you want to run the server in remote mode default is false stdio is used +REMOTE=false + # Private key used to sign requests -PRIVATE_KEY=your_private_key_here +PRIVATE_KEY=your_private_key_heres + +# if PRIVATE_KEY is not set, you can set the path to the keystore file +PRIVATE_KEY_PATH=~/Library/Ethereum/keystore/wallet.json diff --git a/README.md b/README.md index 7893d18..2d08920 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,6 @@ --- -1. Ensure you have [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/get-npm) installed. -2. Clone the repository and install dependencies: - ```bash - git clone https://github.com/iexec-blockchain-computing/iexec-mcp-server.git - cd iexec-mcp-server - npm install - npm run build - ``` - ## 1. Prerequisites - **For Local (Node.js) setup:** @@ -51,7 +42,42 @@ Find your wallet at: --- -## 3. Local (Node.js) +## 3. Quickstart with Claude Code CLI + +The fastest way to get started is using the Claude Code CLI: + +```bash +# Install Claude Code CLI +npm install -g @anthropic-ai/claude-code + +# Add iExec MCP server with your wallet +claude mcp add iexec-mcp --env PRIVATE_KEY_PATH=~/Library/Ethereum/keystore/wallet.json -- npx @paypes/iexec-mcp@latest run + +# run claude +claude +``` + +### 3.1. Direct npx Configuration for Claude Desktop + +You can also configure Claude Desktop directly to use the latest published package via npx: + +```json +{ + "mcpServers": { + "iexec-mcp-server": { + "command": "npx", + "args": ["-y", "@paypes/iexec-mcp@latest"], + "env": { + "PRIVATE_KEY_PATH": "~/Library/Ethereum/keystore/wallet.json" + } + } + } +} +``` + +--- + +## 4. Local (Node.js) Follow these steps to run the iExec MCP Server locally with Node.js and integrate it with Claude Desktop. diff --git a/package-lock.json b/package-lock.json index 52fe81c..3b36f6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,27 @@ { - "name": "iexec-mcp-server", - "version": "1.0.0", + "name": "@paypes/iexec-mcp", + "version": "0.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "iexec-mcp-server", - "version": "1.0.0", - "license": "ISC", + "name": "@paypes/iexec-mcp", + "version": "0.0.4", + "license": "MIT", "dependencies": { "@iexec/dataprotector": "^2.0.0-beta.15", "@iexec/web3mail": "^1.2.1", "@modelcontextprotocol/sdk": "^1.11.3", "ethers": "^6.14.1", + "express": "^5.1.0", "zod": "^3.24.4" }, "bin": { - "weather": "build/index.js" + "iexec-mcp": "build/index.js", + "run": "build/index.js" }, "devDependencies": { + "@types/express": "^5.0.2", "@types/node": "^22.15.18", "dotenv": "^16.5.0", "typescript": "^5.8.3" @@ -548,9 +551,9 @@ } }, "node_modules/@ipld/dag-cbor": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-9.2.3.tgz", - "integrity": "sha512-x78CdVhtctIMBpwOnzbojsvDy6qNxdeR3RC2W+/fMo2sccqlYkPXbjpmghAVdKx/g0xfHYvSMU8cgCnYvLwRag==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-9.2.4.tgz", + "integrity": "sha512-GbDWYl2fdJgkYtIJN0HY9oO0o50d1nB4EQb7uYWKUd2ztxCjxiEW3PjwGG0nqUpN1G4Cug6LX8NzbA7fKT+zfA==", "license": "Apache-2.0 OR MIT", "dependencies": { "cborg": "^4.0.0", @@ -562,9 +565,9 @@ } }, "node_modules/@ipld/dag-json": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/@ipld/dag-json/-/dag-json-10.2.4.tgz", - "integrity": "sha512-u1Pp4Tqz4r9GHGeLIJUJat5slasSxpBiBlL/i5pmGVEkCnQ1plrp5rQ5e+x3wZaPabxvsirQpfR76TPoGAiXiw==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/@ipld/dag-json/-/dag-json-10.2.5.tgz", + "integrity": "sha512-Q4Fr3IBDEN8gkpgNefynJ4U/ZO5Kwr7WSUMBDbZx0c37t0+IwQCTM9yJh8l5L4SRFjm31MuHwniZ/kM+P7GQ3Q==", "license": "Apache-2.0 OR MIT", "dependencies": { "cborg": "^4.0.0", @@ -576,9 +579,9 @@ } }, "node_modules/@ipld/dag-pb": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@ipld/dag-pb/-/dag-pb-4.1.4.tgz", - "integrity": "sha512-v8GLZoFYekDCFpgRgS158S1fkXKWVhF0T6wQJqS+aPyBDewygOjHEUJW7C2cDMw/BNwbMlzzieBwZrt7HWFsyw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@ipld/dag-pb/-/dag-pb-4.1.5.tgz", + "integrity": "sha512-w4PZ2yPqvNmlAir7/2hsCRMqny1EY5jj26iZcSgxREJexmbAc2FI21jp26MqiNdfgAxvkCnf2N/TJI18GaDNwA==", "license": "Apache-2.0 OR MIT", "dependencies": { "multiformats": "^13.1.0" @@ -633,11 +636,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.3.tgz", - "integrity": "sha512-rmOWVRUbUJD7iSvJugjUbFZshTAuJ48MXoZ80Osx1GM0K/H1w7rSEvmw8m6vdWxNASgtaHIhAgre4H/E9GJiYQ==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz", + "integrity": "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==", "license": "MIT", "dependencies": { + "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", @@ -787,6 +791,27 @@ "@types/node": "*" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/dns-packet": { "version": "5.6.5", "resolved": "https://registry.npmjs.org/@types/dns-packet/-/dns-packet-5.6.5.tgz", @@ -796,10 +821,49 @@ "@types/node": "*" } }, + "node_modules/@types/express": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", + "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "22.15.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", - "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -811,6 +875,43 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "license": "MIT" }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@uniswap/lib": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@uniswap/lib/-/lib-1.1.1.tgz", @@ -861,6 +962,22 @@ "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", "license": "MIT" }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1080,9 +1197,9 @@ } }, "node_modules/cborg": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.2.10.tgz", - "integrity": "sha512-ZVA0xrVn8uBfDJYgfKKZzB/93z/Uiz7YtRdBPsZi/gyHNyqFdHMLHURVEk9dejOHepaX0zhcMyNva2/vF972SA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.2.11.tgz", + "integrity": "sha512-7gs3iaqtsD9OHowgqzc6ixQGwSBONqosVR2co0Bg0pARgrLap+LCcEIXJuuIz2jHy0WWQeDMFPEsU2r17I2XPQ==", "license": "Apache-2.0", "bin": { "cborg": "lib/bin.js" @@ -1720,6 +1837,12 @@ "node": ">=0.10.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -1742,6 +1865,12 @@ "node": ">=8.6.0" } }, + "node_modules/fast-json-stable-stringify": { + "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==", + "license": "MIT" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -2429,6 +2558,12 @@ "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", "license": "MIT" }, + "node_modules/json-schema-traverse": { + "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==", + "license": "MIT" + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -2685,9 +2820,9 @@ "license": "MIT" }, "node_modules/multiformats": { - "version": "13.3.4", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.4.tgz", - "integrity": "sha512-JXpM5p9TpJ/BHsUtmLaWuRN0ft0gJPGa6BhkX2KXjFHvkFQOQkDManoar3gx0JsTLNrOojBE2Mj4hFxohGnXZA==", + "version": "13.3.6", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.6.tgz", + "integrity": "sha512-yakbt9cPYj8d3vi/8o/XWm61MrOILo7fsTL0qxNx6zS0Nso6K5JqqS2WV7vK/KSuDBvrW3KfCwAdAgarAgOmww==", "license": "Apache-2.0 OR MIT" }, "node_modules/mute-stream": { @@ -3051,6 +3186,15 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -3067,9 +3211,9 @@ } }, "node_modules/query-string": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.2.tgz", - "integrity": "sha512-s3UlTyjxRux4KjwWaJsjh1Mp8zoCkSGKirbD9H89pEM9UOZsfpRZpdfzvsy2/mGlLfC3NnYVpy2gk7jXITHEtA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.2.0.tgz", + "integrity": "sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ==", "license": "MIT", "dependencies": { "decode-uri-component": "^0.4.1", @@ -3899,9 +4043,9 @@ } }, "node_modules/undici": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.9.0.tgz", - "integrity": "sha512-e696y354tf5cFZPXsF26Yg+5M63+5H3oE6Vtkh2oqbvsE2Oe7s2nIbcQh5lmG7Lp/eS29vJtTpw9+p6PX0qNSg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", + "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", "license": "MIT", "peer": true, "engines": { @@ -3942,6 +4086,15 @@ "registry-url": "3.1.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4161,9 +4314,9 @@ } }, "node_modules/zod": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", - "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "version": "3.25.23", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.23.tgz", + "integrity": "sha512-Od2bdMosahjSrSgJtakrwjMDb1zM1A3VIHCPGveZt/3/wlrTWBya2lmEh2OYe4OIu8mPTmmr0gnLHIWQXdtWBg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 1aa8b19..341b13c 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "@paypes/iexec-mcp", - "version": "0.0.1", + "version": "0.0.4", "description": "MCP server for interacting with iExec Blockchain", "type": "module", "main": "build/index.js", "types": "build/index.d.ts", "bin": { - "iexec-mcp": "build/index.js" + "iexec-mcp": "build/index.js", + "run": "build/index.js" }, "homepage": "https://github.com/iExecBlockchainComputing/iexec-mcp-server", "repository": { @@ -31,9 +32,11 @@ "@iexec/web3mail": "^1.2.1", "@modelcontextprotocol/sdk": "^1.11.3", "ethers": "^6.14.1", + "express": "^5.1.0", "zod": "^3.24.4" }, "devDependencies": { + "@types/express": "^5.0.2", "@types/node": "^22.15.18", "dotenv": "^16.5.0", "typescript": "^5.8.3" diff --git a/src/index.ts b/src/index.ts index 53901a9..72050ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,74 +1,8 @@ #!/usr/bin/env node -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, - McpError, - ErrorCode, -} from "@modelcontextprotocol/sdk/types.js"; -import { protectData } from "./tools/dataProtectorCore/protectData.js"; -import { getUserVoucher } from "./tools/dataProtectorCore/getUserVoucher.js"; -import { getWalletBalance } from "./tools/dataProtectorCore/getWalletBalance.js"; -import { getProtectedData } from "./tools/dataProtectorCore/getProtectedData.js"; -import { transferOwnership } from "./tools/dataProtectorCore/transferOwnership.js"; -import { grantAccess } from "./tools/dataProtectorCore/grantAccess.js"; -import { revokeOneAccess } from "./tools/dataProtectorCore/revokeOneAccess.js"; -import { revokeAllAccess } from "./tools/dataProtectorCore/revokeAllAccess.js"; -import { getGrantedAccess } from "./tools/dataProtectorCore/getGrantedAccess.js"; -import { processProtectedData } from "./tools/dataProtectorCore/processProtectedData.js"; +import { startServer } from './server.js'; -import { fetchMyContacts } from "./tools/web3mail/fetchMyContacts.js"; -import { fetchUserContacts } from "./tools/web3mail/fetchUserContacts.js"; -import { sendEmail } from "./tools/web3mail/sendEmail.js"; - -import { getIExecApps } from "./tools/getIExecApps.js"; - -const server = new Server( - { - name: "iexec-mcp-local-server", - version: "1.0.0", - }, - { - capabilities: { - resources: {}, - tools: {}, - }, - } -); - -const tools = [protectData, getProtectedData, processProtectedData, getIExecApps, transferOwnership, grantAccess, getGrantedAccess, revokeOneAccess, sendEmail, revokeAllAccess, fetchUserContacts, fetchMyContacts, getUserVoucher, getWalletBalance]; - -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: tools.map(({ name, description, inputSchema }) => ({ - name, - description, - inputSchema, - })), - }; -}); - -server.setRequestHandler(CallToolRequestSchema, async (request: any) => { - const tool = tools.find((t) => t.name === request.params.name); - if (!tool) { - throw new McpError(ErrorCode.MethodNotFound, "Tool not found"); - } - - const result = await tool.handler(request.params.arguments); - return { toolResult: result }; -}); - -async function main() { - try { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Server started and listening on stdio"); - } catch (error) { - console.error("Failed to start server:", error); - process.exit(1); - } -} - -main(); +startServer().catch((err) => { + console.error("Failed to start server:", err); + process.exit(1); +}); \ No newline at end of file diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..1232f4c --- /dev/null +++ b/src/server.ts @@ -0,0 +1,158 @@ +import express from 'express'; +import type { Request, Response } from 'express'; +import { + Server +} from "@modelcontextprotocol/sdk/server/index.js"; +import { + StdioServerTransport +} from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + SSEServerTransport +} from "@modelcontextprotocol/sdk/server/sse.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + McpError, + ErrorCode, +} from "@modelcontextprotocol/sdk/types.js"; + +// Import all tools +import { protectData } from "./tools/dataProtectorCore/protectData.js"; +import { getUserVoucher } from "./tools/dataProtectorCore/getUserVoucher.js"; +import { getWalletBalance } from "./tools/dataProtectorCore/getWalletBalance.js"; +import { getProtectedData } from "./tools/dataProtectorCore/getProtectedData.js"; +import { transferOwnership } from "./tools/dataProtectorCore/transferOwnership.js"; +import { grantAccess } from "./tools/dataProtectorCore/grantAccess.js"; +import { revokeOneAccess } from "./tools/dataProtectorCore/revokeOneAccess.js"; +import { revokeAllAccess } from "./tools/dataProtectorCore/revokeAllAccess.js"; +import { getGrantedAccess } from "./tools/dataProtectorCore/getGrantedAccess.js"; +import { processProtectedData } from "./tools/dataProtectorCore/processProtectedData.js"; +import { fetchMyContacts } from "./tools/web3mail/fetchMyContacts.js"; +import { fetchUserContacts } from "./tools/web3mail/fetchUserContacts.js"; +import { sendEmail } from "./tools/web3mail/sendEmail.js"; +import { getIExecApps } from "./tools/getIExecApps.js"; + +const tools = [ + protectData, + getProtectedData, + processProtectedData, + getIExecApps, + transferOwnership, + grantAccess, + getGrantedAccess, + revokeOneAccess, + sendEmail, + revokeAllAccess, + fetchUserContacts, + fetchMyContacts, + getUserVoucher, + getWalletBalance, +]; + +export async function startServer(): Promise { + const remote = process.env.REMOTE ?? "false"; + const port = process.env.PORT ?? 3000; + + const server = new Server( + { + name: "iexec-mcp-server", + version: "1.0.0", + }, + { + capabilities: { + resources: {}, + tools: {}, + }, + } + ); + + // Register tools + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: tools.map(({ name, description, inputSchema }) => ({ + name, + description, + inputSchema, + })), + })); + + server.setRequestHandler(CallToolRequestSchema, async (request: any) => { + const tool = tools.find((t) => t.name === request.params.name); + if (!tool) { + throw new McpError(ErrorCode.MethodNotFound, "Tool not found"); + } + const result = await tool.handler(request.params.arguments); + return { + content: [ + { + type: "text", + text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) + } + ] + }; + }); + + if (remote === "true") { + const app = express(); + app.use(express.json()); + + const transports = new Map(); + + app.get('/sse', async (req: Request, res: Response) => { + try { + const transport = new SSEServerTransport('/messages', res); + const sessionId = transport.sessionId; + + transports.set(sessionId, transport); + console.log(`SSE client connected: ${sessionId}`); + + req.on('close', () => { + console.log(`SSE client disconnected: ${sessionId}`); + transports.delete(sessionId); + }); + + await server.connect(transport); + + await server.sendLoggingMessage({ + level: 'info', + data: { message: 'iExec MCP server started with SSE transport...' }, + }); + } catch (error) { + console.error('Error handling SSE connection:', error); + res.status(500).send('Error establishing SSE connection'); + } + }); + + app.post('/messages', async (req, res) => { + try { + const sessionId = req.query.sessionId as string; + if (!sessionId) { + res.status(400).send('Missing sessionId parameter'); + return; + } + + const transport = transports.get(sessionId); + if (!transport) { + res.status(400).json({ error: 'No active SSE connection found' }); + return; + } + + await transport.handlePostMessage(req, res, req.body); + } catch (error) { + res.status(500).json({ + error: 'Failed to process message', + message: error instanceof Error ? error.message : String(error), + }); + } + }); + + app.listen(port, () => { + console.log(`iExec MCP server listening on port ${port}`); + console.log(`SSE endpoint: http://localhost:${port}/sse`); + console.log(`Message endpoint: http://localhost:${port}/messages`); + }); + } else { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('iExec MCP server started with stdio transport...'); + } +} diff --git a/src/tools/dataProtectorCore/getGrantedAccess.ts b/src/tools/dataProtectorCore/getGrantedAccess.ts index b63939d..8f9253b 100644 --- a/src/tools/dataProtectorCore/getGrantedAccess.ts +++ b/src/tools/dataProtectorCore/getGrantedAccess.ts @@ -1,5 +1,6 @@ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { getWeb3Provider, IExecDataProtectorCore } from "@iexec/dataprotector"; +import { readWalletPrivateKey } from "../../utils/readWalletKeystore.js"; export const getGrantedAccess = { name: "get_granted_access", @@ -17,14 +18,10 @@ export const getGrantedAccess = { required: [], }, handler: async (params: any) => { - const privateKey = process.env.PRIVATE_KEY as string; - - if (!privateKey) { - throw new McpError(ErrorCode.InternalError, "Missing PRIVATE_KEY in environment variables"); - } const { protectedData, authorizedApp, authorizedUser } = params; try { + const privateKey = await readWalletPrivateKey(); const web3Provider = getWeb3Provider(privateKey); const dataProtectorCore = new IExecDataProtectorCore(web3Provider); diff --git a/src/tools/dataProtectorCore/getProtectedData.ts b/src/tools/dataProtectorCore/getProtectedData.ts index f5711fa..c2838ee 100644 --- a/src/tools/dataProtectorCore/getProtectedData.ts +++ b/src/tools/dataProtectorCore/getProtectedData.ts @@ -4,6 +4,7 @@ import { getWeb3Provider, IExecDataProtectorCore, } from "@iexec/dataprotector"; +import { readWalletPrivateKey } from "../../utils/readWalletKeystore.js"; export const getProtectedData = { name: "get_protected_data", @@ -22,13 +23,8 @@ export const getProtectedData = { required: [], }, handler: async () => { - const privateKey = process.env.PRIVATE_KEY as string; - - if (!privateKey) { - throw new McpError(ErrorCode.InternalError, "Missing PRIVATE_KEY in environment variables"); - } - try { + const privateKey = await readWalletPrivateKey(); const wallet = new Wallet(privateKey); const web3Provider = getWeb3Provider(privateKey); diff --git a/src/tools/dataProtectorCore/grantAccess.ts b/src/tools/dataProtectorCore/grantAccess.ts index 47b558a..87791a3 100644 --- a/src/tools/dataProtectorCore/grantAccess.ts +++ b/src/tools/dataProtectorCore/grantAccess.ts @@ -1,5 +1,6 @@ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { getWeb3Provider, IExecDataProtectorCore } from "@iexec/dataprotector"; +import { readWalletPrivateKey } from "../../utils/readWalletKeystore.js"; // Dictionnaire d'apps connues avec alias const iExecApps = { @@ -58,11 +59,6 @@ export const grantAccess = { }, handler: async (params: { protectedData: any; authorizedApp: any; authorizedUser: any; pricePerAccess: any; numberOfAccess: any; }) => { - const privateKey = process.env.PRIVATE_KEY as string; - - if (!privateKey) { - throw new McpError(ErrorCode.InternalError, "Missing PRIVATE_KEY in environment variables"); - } const { protectedData, authorizedApp, @@ -83,7 +79,7 @@ export const grantAccess = { } try { - + const privateKey = await readWalletPrivateKey(); const resolvedApp = resolveApp(authorizedApp); console.error("Resolved app:", resolvedApp); const web3Provider = getWeb3Provider(privateKey); diff --git a/src/tools/dataProtectorCore/processProtectedData.ts b/src/tools/dataProtectorCore/processProtectedData.ts index 3e49700..e81f5f5 100644 --- a/src/tools/dataProtectorCore/processProtectedData.ts +++ b/src/tools/dataProtectorCore/processProtectedData.ts @@ -1,5 +1,6 @@ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { getWeb3Provider, IExecDataProtectorCore } from "@iexec/dataprotector"; +import { readWalletPrivateKey } from "../../utils/readWalletKeystore.js"; export const processProtectedData = { name: "process_protected_data", @@ -29,11 +30,6 @@ export const processProtectedData = { required: ["protectedData", "app"], }, handler: async (params: any) => { - const privateKey = process.env.PRIVATE_KEY as string; - - if (!privateKey) { - throw new McpError(ErrorCode.InternalError, "Missing PRIVATE_KEY in environment variables"); - } const { protectedData, app, @@ -56,6 +52,7 @@ export const processProtectedData = { } try { + const privateKey = await readWalletPrivateKey(); const web3Provider = getWeb3Provider(privateKey); const dataProtectorCore = new IExecDataProtectorCore(web3Provider); diff --git a/src/tools/dataProtectorCore/protectData.ts b/src/tools/dataProtectorCore/protectData.ts index fcf8a33..eae64f9 100644 --- a/src/tools/dataProtectorCore/protectData.ts +++ b/src/tools/dataProtectorCore/protectData.ts @@ -1,7 +1,7 @@ import { getWeb3Provider, IExecDataProtectorCore } from "@iexec/dataprotector"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { Wallet } from "ethers"; -import { z } from "zod"; +import { readWalletPrivateKey } from "../../utils/readWalletKeystore.js"; export const protectData = { name: "protect_data", @@ -10,23 +10,21 @@ export const protectData = { inputSchema: { type: "object", properties: { - data: { type: "object" }, // Required: JSON object to protect - name: { type: "string" }, // Optional: Public name for the protected data - allowDebug: { type: "boolean" }, // Optional: Enable for dev/testing + data: { type: "object" }, + name: { type: "string" }, + allowDebug: { type: "boolean" }, }, required: ["data"], }, handler: async (params: any) => { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new McpError(ErrorCode.InternalError, "Missing PRIVATE_KEY in environment variables"); - } + const { data, name = "", allowDebug = false } = params; if (typeof data !== "object" || Array.isArray(data) || data === null) { throw new McpError(ErrorCode.InvalidParams, "Parameter 'data' must be a JSON object"); } try { + const privateKey = await readWalletPrivateKey(); const signer = new Wallet(privateKey); const web3Provider = getWeb3Provider(signer.privateKey); const dataProtectorCore = new IExecDataProtectorCore(web3Provider); diff --git a/src/tools/dataProtectorCore/revokeAllAccess.ts b/src/tools/dataProtectorCore/revokeAllAccess.ts index e866cd3..e6b66c1 100644 --- a/src/tools/dataProtectorCore/revokeAllAccess.ts +++ b/src/tools/dataProtectorCore/revokeAllAccess.ts @@ -1,5 +1,6 @@ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { getWeb3Provider, IExecDataProtectorCore } from "@iexec/dataprotector"; +import { readWalletPrivateKey } from "../../utils/readWalletKeystore.js"; export const revokeAllAccess = { name: "revoke_all_access", @@ -13,12 +14,6 @@ export const revokeAllAccess = { required: ["protectedData"], }, handler: async (params: any) => { - const privateKey = process.env.PRIVATE_KEY as string; - - if (!privateKey) { - throw new McpError(ErrorCode.InternalError, "Missing PRIVATE_KEY in environment variables"); - } - const { protectedData } = params; if (typeof protectedData !== "string") { throw new McpError( @@ -28,6 +23,7 @@ export const revokeAllAccess = { } try { + const privateKey = await readWalletPrivateKey(); const web3Provider = getWeb3Provider(privateKey); const dataProtectorCore = new IExecDataProtectorCore(web3Provider); diff --git a/src/tools/dataProtectorCore/revokeOneAccess.ts b/src/tools/dataProtectorCore/revokeOneAccess.ts index 4ad23b9..750804d 100644 --- a/src/tools/dataProtectorCore/revokeOneAccess.ts +++ b/src/tools/dataProtectorCore/revokeOneAccess.ts @@ -1,5 +1,6 @@ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { getWeb3Provider, IExecDataProtectorCore } from "@iexec/dataprotector"; +import { readWalletPrivateKey } from "../../utils/readWalletKeystore.js"; export const revokeOneAccess = { name: "revoke_access", @@ -14,11 +15,6 @@ export const revokeOneAccess = { required: ["protectedData", "authorizedApp", "authorizedUser"], }, handler: async (params: any) => { - const privateKey = process.env.PRIVATE_KEY as string; - - if (!privateKey) { - throw new McpError(ErrorCode.InternalError, "Missing PRIVATE_KEY in environment variables"); - } const { protectedData, authorizedApp, authorizedUser } = params; if ( @@ -33,6 +29,7 @@ export const revokeOneAccess = { } try { + const privateKey = await readWalletPrivateKey(); const web3Provider = getWeb3Provider(privateKey); const dataProtectorCore = new IExecDataProtectorCore(web3Provider); diff --git a/src/tools/dataProtectorCore/transferOwnership.ts b/src/tools/dataProtectorCore/transferOwnership.ts index ff90d18..5b55e68 100644 --- a/src/tools/dataProtectorCore/transferOwnership.ts +++ b/src/tools/dataProtectorCore/transferOwnership.ts @@ -2,6 +2,7 @@ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { IExecDataProtectorCore, } from "@iexec/dataprotector"; +import { readWalletPrivateKey } from "../../utils/readWalletKeystore.js"; export const transferOwnership = { name: "transfer_ownership", @@ -15,11 +16,7 @@ export const transferOwnership = { required: ["protectedData", "newOwner"], }, handler: async (params: any) => { - const privateKey = process.env.PRIVATE_KEY as string; - if (!privateKey) { - throw new McpError(ErrorCode.InternalError, "Missing PRIVATE_KEY in environment variables"); - } const { protectedData, newOwner } = params; if (typeof protectedData !== "string" || typeof newOwner !== "string") { @@ -30,6 +27,7 @@ export const transferOwnership = { } try { + const privateKey = await readWalletPrivateKey(); const dataProtectorCore = new IExecDataProtectorCore(privateKey); const result = await dataProtectorCore.transferOwnership({ protectedData, diff --git a/src/tools/web3mail/fetchMyContacts.ts b/src/tools/web3mail/fetchMyContacts.ts index 4a99918..daad7ff 100644 --- a/src/tools/web3mail/fetchMyContacts.ts +++ b/src/tools/web3mail/fetchMyContacts.ts @@ -1,5 +1,6 @@ import { getWeb3Provider, IExecWeb3mail } from "@iexec/web3mail"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; +import { readWalletPrivateKey } from "../../utils/readWalletKeystore.js"; export const fetchMyContacts = { name: "fetchMyContacts", @@ -10,13 +11,8 @@ export const fetchMyContacts = { }, }, handler: async () => { - const privateKey = process.env.PRIVATE_KEY as string; - - if (!privateKey) { - throw new McpError(ErrorCode.InternalError, "Missing PRIVATE_KEY in environment variables"); - } - try { + const privateKey = await readWalletPrivateKey(); const web3Provider = getWeb3Provider(privateKey); const web3mail = new IExecWeb3mail(web3Provider); return await web3mail.fetchMyContacts(); diff --git a/src/tools/web3mail/fetchUserContacts.ts b/src/tools/web3mail/fetchUserContacts.ts index 06efca7..77488b6 100644 --- a/src/tools/web3mail/fetchUserContacts.ts +++ b/src/tools/web3mail/fetchUserContacts.ts @@ -1,5 +1,6 @@ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { getWeb3Provider, IExecWeb3mail, WorkflowError } from '@iexec/web3mail'; +import { readWalletPrivateKey } from '../../utils/readWalletKeystore.js'; export const fetchUserContacts = { name: 'fetchUserContacts', @@ -12,11 +13,8 @@ export const fetchUserContacts = { required: ['userAddress'], }, handler: async (params: any) => { - const privateKey = process.env.PRIVATE_KEY as string; + const privateKey = await readWalletPrivateKey(); - if (!privateKey) { - throw new McpError(ErrorCode.InternalError, "Missing PRIVATE_KEY in environment variables"); - } const { userAddress } = params; if (typeof userAddress !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Invalid or missing parameters'); diff --git a/src/tools/web3mail/sendEmail.ts b/src/tools/web3mail/sendEmail.ts index a23af38..90c3187 100644 --- a/src/tools/web3mail/sendEmail.ts +++ b/src/tools/web3mail/sendEmail.ts @@ -1,5 +1,6 @@ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { IExecWeb3mail, WorkflowError } from '@iexec/web3mail'; +import { readWalletPrivateKey } from '../../utils/readWalletKeystore.js'; export const sendEmail = { name: 'sendEmail', @@ -20,11 +21,6 @@ export const sendEmail = { required: ['emailSubject', 'emailContent', 'protectedData'], }, handler: async (params: any) => { - const privateKey = process.env.PRIVATE_KEY as string; - - if (!privateKey) { - throw new McpError(ErrorCode.InternalError, "Missing PRIVATE_KEY in environment variables"); - } const { emailSubject, emailContent, @@ -46,6 +42,7 @@ export const sendEmail = { } try { + const privateKey = await readWalletPrivateKey(); const web3mail = new IExecWeb3mail(privateKey); const sendEmailParams: any = { diff --git a/src/utils/readWalletKeystore.ts b/src/utils/readWalletKeystore.ts new file mode 100644 index 0000000..0643d2b --- /dev/null +++ b/src/utils/readWalletKeystore.ts @@ -0,0 +1,36 @@ +import { promises as fs } from 'fs'; +import os from 'os'; + +export async function readWalletPrivateKey() { + const envPrivateKey = process.env.PRIVATE_KEY; + const rawPath = process.env.PRIVATE_KEY_PATH; + + if (envPrivateKey) { + return envPrivateKey; + } + + if (!rawPath) { + console.error('❌ Neither PRIVATE_KEY nor PRIVATE_KEY_PATH is set in your .env'); + return null; + } + + const keyPath = rawPath.replace(/^~\//, `${os.homedir()}/`); + + try { + const fileContent = await fs.readFile(keyPath, 'utf-8'); + const wallet = JSON.parse(fileContent); + + if (!wallet.privateKey) { + throw new Error('Missing "privateKey" in wallet.json'); + } + + return wallet.privateKey; + } catch (error) { + if (error instanceof Error) { + console.error(`❌ Failed to load wallet from ${keyPath}: ${error.message}`); + } else { + console.error(`❌ Failed to load wallet from ${keyPath}:`, error); + } + return null; + } +}