From 25ab4d9d516298c0847968759fbe814283735945 Mon Sep 17 00:00:00 2001 From: Valentin Cart Date: Wed, 19 Nov 2025 10:45:01 -0500 Subject: [PATCH 01/11] feat: Update MCP SDK, configure server capabilities, and bump server version to 2.0.0. --- bun.lock | 278 +++++--- package.json | 20 +- src/core/chains.ts | 5 +- src/core/prompts.ts | 87 ++- src/core/resources.ts | 486 ++++---------- src/core/tools.ts | 1340 ++++++++++++++++--------------------- src/server/http-server.ts | 277 ++++---- src/server/server.ts | 38 +- tsconfig.json | 8 +- 9 files changed, 1102 insertions(+), 1437 deletions(-) diff --git a/bun.lock b/bun.lock index 40cb91b..d0e577c 100644 --- a/bun.lock +++ b/bun.lock @@ -4,16 +4,14 @@ "": { "name": "mcp-evm-server", "dependencies": { - "@modelcontextprotocol/sdk": "^1.6.1", - "cors": "^2.8.5", - "express": "^4.21.2", - "viem": "^2.23.8", - "zod": "^3.24.2", + "@modelcontextprotocol/sdk": "^1.22.0", + "viem": "^2.39.3", + "zod": "^3.24.3", }, "devDependencies": { "@types/bun": "latest", - "@types/cors": "^2.8.17", - "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "conventional-changelog-cli": "^5.0.0", }, "peerDependencies": { "typescript": "^5.8.2", @@ -23,53 +21,51 @@ "packages": { "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.0", "", {}, "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.7.0", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@conventional-changelog/git-client": ["@conventional-changelog/git-client@1.0.1", "", { "dependencies": { "@types/semver": "^7.5.5", "semver": "^7.5.2" }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.0.0" }, "optionalPeers": ["conventional-commits-filter", "conventional-commits-parser"] }, "sha512-PJEqBwAleffCMETaVm/fUgHldzBE35JFk3/9LL6NUA5EXa3qednu+UT6M7E5iBu3zIQZCULYIiZ90fBYHt6xUw=="], - "@scure/base": ["@scure/base@1.2.4", "", {}, "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ=="], + "@hutson/parse-repository-url": ["@hutson/parse-repository-url@5.0.0", "", {}, "sha512-e5+YUKENATs1JgYHMzTr2MW/NDcXGfYFAuOQU8gJgF/kEh4EqKgfGrfLI67bMD4tbhZVlkigz/9YYwWcbOFthg=="], - "@scure/bip32": ["@scure/bip32@1.6.2", "", { "dependencies": { "@noble/curves": "~1.8.1", "@noble/hashes": "~1.7.1", "@scure/base": "~1.2.2" } }, "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.22.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g=="], - "@scure/bip39": ["@scure/bip39@1.5.4", "", { "dependencies": { "@noble/hashes": "~1.7.1", "@scure/base": "~1.2.4" } }, "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA=="], + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], - "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], + "@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], - "@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="], - - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - - "@types/cors": ["@types/cors@2.8.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA=="], + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@types/express": ["@types/express@5.0.0", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ=="], + "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="], + "@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], - "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], + "@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], - "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="], "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - "@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="], + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], - "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], - "@types/send": ["@types/send@0.17.4", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA=="], + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], - "@types/serve-static": ["@types/serve-static@1.15.7", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw=="], + "abitype": ["abitype@1.1.0", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A=="], - "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "abitype": ["abitype@1.0.8", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg=="], + "add-stream": ["add-stream@1.0.0", "", {}, "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ=="], - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "body-parser": ["body-parser@1.20.3", "", { "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.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + "array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], + + "body-parser": ["body-parser@2.1.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.5.2", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ=="], "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], @@ -79,22 +75,60 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], + + "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "conventional-changelog": ["conventional-changelog@6.0.0", "", { "dependencies": { "conventional-changelog-angular": "^8.0.0", "conventional-changelog-atom": "^5.0.0", "conventional-changelog-codemirror": "^5.0.0", "conventional-changelog-conventionalcommits": "^8.0.0", "conventional-changelog-core": "^8.0.0", "conventional-changelog-ember": "^5.0.0", "conventional-changelog-eslint": "^6.0.0", "conventional-changelog-express": "^5.0.0", "conventional-changelog-jquery": "^6.0.0", "conventional-changelog-jshint": "^5.0.0", "conventional-changelog-preset-loader": "^5.0.0" } }, "sha512-tuUH8H/19VjtD9Ig7l6TQRh+Z0Yt0NZ6w/cCkkyzUbGQTnUEmKfGtkC9gGfVgCfOL1Rzno5NgNF4KY8vR+Jo3w=="], + + "conventional-changelog-angular": ["conventional-changelog-angular@8.1.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w=="], + + "conventional-changelog-atom": ["conventional-changelog-atom@5.0.0", "", {}, "sha512-WfzCaAvSCFPkznnLgLnfacRAzjgqjLUjvf3MftfsJzQdDICqkOOpcMtdJF3wTerxSpv2IAAjX8doM3Vozqle3g=="], + + "conventional-changelog-cli": ["conventional-changelog-cli@5.0.0", "", { "dependencies": { "add-stream": "^1.0.0", "conventional-changelog": "^6.0.0", "meow": "^13.0.0", "tempfile": "^5.0.0" }, "bin": { "conventional-changelog": "cli.js" } }, "sha512-9Y8fucJe18/6ef6ZlyIlT2YQUbczvoQZZuYmDLaGvcSBP+M6h+LAvf7ON7waRxKJemcCII8Yqu5/8HEfskTxJQ=="], + + "conventional-changelog-codemirror": ["conventional-changelog-codemirror@5.0.0", "", {}, "sha512-8gsBDI5Y3vrKUCxN6Ue8xr6occZ5nsDEc4C7jO/EovFGozx8uttCAyfhRrvoUAWi2WMm3OmYs+0mPJU7kQdYWQ=="], + + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@8.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-eOvlTO6OcySPyyyk8pKz2dP4jjElYunj9hn9/s0OB+gapTO8zwS9UQWrZ1pmF2hFs3vw1xhonOLGcGjy/zgsuA=="], + + "conventional-changelog-core": ["conventional-changelog-core@8.0.0", "", { "dependencies": { "@hutson/parse-repository-url": "^5.0.0", "add-stream": "^1.0.0", "conventional-changelog-writer": "^8.0.0", "conventional-commits-parser": "^6.0.0", "git-raw-commits": "^5.0.0", "git-semver-tags": "^8.0.0", "hosted-git-info": "^7.0.0", "normalize-package-data": "^6.0.0", "read-package-up": "^11.0.0", "read-pkg": "^9.0.0" } }, "sha512-EATUx5y9xewpEe10UEGNpbSHRC6cVZgO+hXQjofMqpy+gFIrcGvH3Fl6yk2VFKh7m+ffenup2N7SZJYpyD9evw=="], + + "conventional-changelog-ember": ["conventional-changelog-ember@5.0.0", "", {}, "sha512-RPflVfm5s4cSO33GH/Ey26oxhiC67akcxSKL8CLRT3kQX2W3dbE19sSOM56iFqUJYEwv9mD9r6k79weWe1urfg=="], + + "conventional-changelog-eslint": ["conventional-changelog-eslint@6.0.0", "", {}, "sha512-eiUyULWjzq+ybPjXwU6NNRflApDWlPEQEHvI8UAItYW/h22RKkMnOAtfCZxMmrcMO1OKUWtcf2MxKYMWe9zJuw=="], + + "conventional-changelog-express": ["conventional-changelog-express@5.0.0", "", {}, "sha512-D8Q6WctPkQpvr2HNCCmwU5GkX22BVHM0r4EW8vN0230TSyS/d6VQJDAxGb84lbg0dFjpO22MwmsikKL++Oo/oQ=="], + + "conventional-changelog-jquery": ["conventional-changelog-jquery@6.0.0", "", {}, "sha512-2kxmVakyehgyrho2ZHBi90v4AHswkGzHuTaoH40bmeNqUt20yEkDOSpw8HlPBfvEQBwGtbE+5HpRwzj6ac2UfA=="], + + "conventional-changelog-jshint": ["conventional-changelog-jshint@5.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-gGNphSb/opc76n2eWaO6ma4/Wqu3tpa2w7i9WYqI6Cs2fncDSI2/ihOfMvXveeTTeld0oFvwMVNV+IYQIk3F3g=="], + + "conventional-changelog-preset-loader": ["conventional-changelog-preset-loader@5.0.0", "", {}, "sha512-SetDSntXLk8Jh1NOAl1Gu5uLiCNSYenB5tm0YVeZKePRIgDW9lQImromTwLa3c/Gae298tsgOM+/CYT9XAl0NA=="], + + "conventional-changelog-writer": ["conventional-changelog-writer@8.2.0", "", { "dependencies": { "conventional-commits-filter": "^5.0.0", "handlebars": "^4.7.7", "meow": "^13.0.0", "semver": "^7.5.2" }, "bin": { "conventional-changelog-writer": "dist/cli/index.js" } }, "sha512-Y2aW4596l9AEvFJRwFGJGiQjt2sBYTjPD18DdvxX9Vpz0Z7HQ+g1Z+6iYDAm1vR3QOJrDBkRHixHK/+FhkR6Pw=="], + + "conventional-commits-filter": ["conventional-commits-filter@5.0.0", "", {}, "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q=="], + + "conventional-commits-parser": ["conventional-commits-parser@6.2.1", "", { "dependencies": { "meow": "^13.0.0" }, "bin": { "conventional-commits-parser": "dist/cli/index.js" } }, "sha512-20pyHgnO40rvfI0NGF/xiEoFMkXDtkF8FwHvk5BokoFoCuTQRI8vrNCNFWUOfuolKJMm1tPCHc8GgYEtr1XRNA=="], + "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], - "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], - "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.3.6", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -117,15 +151,21 @@ "eventsource-parser": ["eventsource-parser@3.0.0", "", {}, "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA=="], - "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "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.13.0", "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" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + "express": ["express@5.0.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.0.1", "content-disposition": "^1.0.0", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "^1.2.1", "debug": "4.3.6", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "^2.0.0", "fresh": "2.0.0", "http-errors": "2.0.0", "merge-descriptors": "^2.0.0", "methods": "~1.1.2", "mime-types": "^3.0.0", "on-finished": "2.4.1", "once": "1.4.0", "parseurl": "~1.3.3", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "router": "^2.0.0", "safe-buffer": "5.2.1", "send": "^1.1.0", "serve-static": "^2.1.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "^2.0.0", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ=="], "express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], - "finalhandler": ["finalhandler@1.3.1", "", { "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.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + + "find-up-simple": ["find-up-simple@1.0.1", "", {}, "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -133,41 +173,67 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "git-raw-commits": ["git-raw-commits@5.0.0", "", { "dependencies": { "@conventional-changelog/git-client": "^1.0.0", "meow": "^13.0.0" }, "bin": { "git-raw-commits": "src/cli.js" } }, "sha512-I2ZXrXeOc0KrCvC7swqtIFXFN+rbjnC7b2T943tvemIOVNl+XP8YnA9UVwqFhzzLClnSA60KR/qEjLpXzs73Qg=="], + + "git-semver-tags": ["git-semver-tags@8.0.0", "", { "dependencies": { "@conventional-changelog/git-client": "^1.0.0", "meow": "^13.0.0" }, "bin": { "git-semver-tags": "src/cli.js" } }, "sha512-N7YRIklvPH3wYWAR2vysaqGLPRcpwQ0GKdlqTiVN5w1UmCdaeY3K8s6DMKRCh54DDdzyt/OAB6C8jgVtb7Y2Fg=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - "isows": ["isows@1.0.6", "", { "peerDependencies": { "ws": "*" } }, "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="], - "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], "mime-db": ["mime-db@1.53.0", "", {}, "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg=="], "mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="], - "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -177,13 +243,19 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "ox": ["ox@0.6.9", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug=="], + "ox": ["ox@0.9.6", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.9", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg=="], + + "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], + "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -193,18 +265,30 @@ "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + "read-package-up": ["read-package-up@11.0.0", "", { "dependencies": { "find-up-simple": "^1.0.0", "read-pkg": "^9.0.0", "type-fest": "^4.6.0" } }, "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ=="], + + "read-pkg": ["read-pkg@9.0.1", "", { "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", "parse-json": "^8.0.0", "type-fest": "^4.6.0", "unicorn-magic": "^0.1.0" } }, "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "router": ["router@2.1.0", "", { "dependencies": { "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "send": ["send@1.1.0", "", { "dependencies": { "debug": "^4.3.5", "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.0", "mime-types": "^2.1.35", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA=="], - "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + "serve-static": ["serve-static@2.1.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.0.0" } }, "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -213,98 +297,76 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - - "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], - - "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "viem": ["viem@2.23.9", "", { "dependencies": { "@noble/curves": "1.8.1", "@noble/hashes": "1.7.1", "@scure/bip32": "1.6.2", "@scure/bip39": "1.5.4", "abitype": "1.0.8", "isows": "1.0.6", "ox": "0.6.9", "ws": "8.18.1" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-y8VLPfKukrstZKTerS9bm45ajZ22wUyStF+VquK3I2OovWLOyXSbQmJWei8syMFhp1uwhxh1tb0fAdx0WSRZWg=="], + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], - "ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], + "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], - "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + "spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="], - "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], - - "@modelcontextprotocol/sdk/express": ["express@5.0.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.0.1", "content-disposition": "^1.0.0", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "^1.2.1", "debug": "4.3.6", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "^2.0.0", "fresh": "2.0.0", "http-errors": "2.0.0", "merge-descriptors": "^2.0.0", "methods": "~1.1.2", "mime-types": "^3.0.0", "on-finished": "2.4.1", "once": "1.4.0", "parseurl": "~1.3.3", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "router": "^2.0.0", "safe-buffer": "5.2.1", "send": "^1.1.0", "serve-static": "^2.1.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "^2.0.0", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ=="], - - "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "body-parser/raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - "router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + "temp-dir": ["temp-dir@3.0.0", "", {}, "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw=="], - "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + "tempfile": ["tempfile@5.0.0", "", { "dependencies": { "temp-dir": "^3.0.0" } }, "sha512-bX655WZI/F7EoTDw9JvQURqAXiPHi8o8+yFxPF2lWYyz1aHnmMRuXWqL6YB6GmeO0o4DIYWHLgGNi/X64T+X4Q=="], - "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "type-is": ["type-is@2.0.0", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw=="], - "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.1.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.5.2", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ=="], + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], - "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], - "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], - "@modelcontextprotocol/sdk/express/debug": ["debug@4.3.6", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg=="], + "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], - "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], - "@modelcontextprotocol/sdk/express/send": ["send@1.1.0", "", { "dependencies": { "debug": "^4.3.5", "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.0", "mime-types": "^2.1.35", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.1.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.0.0" } }, "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA=="], + "viem": ["viem@2.39.3", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.1.0", "isows": "1.0.7", "ox": "0.9.6", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-s11rPQRvUEdc5qHK3xT4fIk4qvgPAaLwaTFq+EbFlcJJD+Xn3R4mc9H6B6fquEiHl/mdsdbG/uKCnYpoNtHNHw=="], - "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.0", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "@modelcontextprotocol/sdk/express/body-parser/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.5.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag=="], + "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], - "@modelcontextprotocol/sdk/express/body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "body-parser/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "@modelcontextprotocol/sdk/express/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + "body-parser/iconv-lite": ["iconv-lite@0.5.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag=="], - "@modelcontextprotocol/sdk/express/finalhandler/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], - "@modelcontextprotocol/sdk/express/send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "finalhandler/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "@modelcontextprotocol/sdk/express/send/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - "@modelcontextprotocol/sdk/express/send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "send/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "@modelcontextprotocol/sdk/express/body-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "body-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "@modelcontextprotocol/sdk/express/finalhandler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "finalhandler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "@modelcontextprotocol/sdk/express/send/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "send/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], } } diff --git a/package.json b/package.json index ab33609..294ba07 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "@mcpdotdirect/evm-mcp-server", "module": "src/index.ts", "type": "module", - "version": "1.2.0", - "description": "Model Context Protocol (MCP) server for interacting with EVM-compatible networks", + "version": "2.0.0", + "description": "MCP server for interacting with EVM-compatible blockchains - supports 28 tools across 86+ networks", "bin": { "evm-mcp-server": "./bin/cli.js" }, @@ -27,23 +27,21 @@ "version:major": "npm version major", "release": "npm publish", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", - "changelog:latest": "conventional-changelog -p angular -r 1 > RELEASE_NOTES.md" + "changelog:latest": "conventional-changelog -p angular -r 1 > RELEASE_NOTES.md", + "inspect": "npx @modelcontextprotocol/inspector node build/index.js" }, "devDependencies": { "@types/bun": "latest", - "@types/cors": "^2.8.17", - "@types/express": "^5.0.0", + "@types/node": "^22.0.0", "conventional-changelog-cli": "^5.0.0" }, "peerDependencies": { "typescript": "^5.8.2" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.7.0", - "cors": "^2.8.5", - "express": "^4.21.2", - "viem": "^2.23.9", - "zod": "^3.24.2" + "@modelcontextprotocol/sdk": "^1.22.0", + "viem": "^2.39.3", + "zod": "^3.24.3" }, "keywords": [ "mcp", @@ -59,7 +57,7 @@ "author": "Etheral ", "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "repository": { "type": "git", diff --git a/src/core/chains.ts b/src/core/chains.ts index c5c5dda..75115d3 100644 --- a/src/core/chains.ts +++ b/src/core/chains.ts @@ -296,8 +296,9 @@ export function resolveChainId(chainIdentifier: number | string): number { const networkName = chainIdentifier.toLowerCase(); // Check if the network name is in our map - if (networkName in networkNameMap) { - return networkNameMap[networkName]; + const chainId = networkNameMap[networkName]; + if (chainId !== undefined) { + return chainId; } // Try parsing as a number diff --git a/src/core/prompts.ts b/src/core/prompts.ts index b7f7bce..fd9dc52 100644 --- a/src/core/prompts.ts +++ b/src/core/prompts.ts @@ -7,19 +7,21 @@ import { z } from "zod"; */ export function registerEVMPrompts(server: McpServer) { // Basic block explorer prompt - server.prompt( + server.registerPrompt( "explore_block", - "Explore information about a specific block", { - blockNumber: z.string().optional().describe("Block number to explore. If not provided, latest block will be used."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Explore information about a specific block", + argsSchema: { + blockNumber: z.string().optional().describe("Block number to explore. If not provided, latest block will be used."), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + } }, ({ blockNumber, network = "ethereum" }) => ({ messages: [{ role: "user", content: { type: "text", - text: blockNumber + text: blockNumber ? `Please analyze block #${blockNumber} on the ${network} network and provide information about its key metrics, transactions, and significance.` : `Please analyze the latest block on the ${network} network and provide information about its key metrics, transactions, and significance.` } @@ -28,12 +30,14 @@ export function registerEVMPrompts(server: McpServer) { ); // Transaction analysis prompt - server.prompt( + server.registerPrompt( "analyze_transaction", - "Analyze a specific transaction", { - txHash: z.string().describe("Transaction hash to analyze"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Analyze a specific transaction", + argsSchema: { + txHash: z.string().describe("Transaction hash to analyze"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + } }, ({ txHash, network = "ethereum" }) => ({ messages: [{ @@ -47,12 +51,14 @@ export function registerEVMPrompts(server: McpServer) { ); // Address analysis prompt - server.prompt( + server.registerPrompt( "analyze_address", - "Analyze an EVM address", { - address: z.string().describe("Ethereum address to analyze"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Analyze an EVM address", + argsSchema: { + address: z.string().describe("Ethereum address to analyze"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + } }, ({ address, network = "ethereum" }) => ({ messages: [{ @@ -66,13 +72,15 @@ export function registerEVMPrompts(server: McpServer) { ); // Smart contract interaction guidance - server.prompt( + server.registerPrompt( "interact_with_contract", - "Get guidance on interacting with a smart contract", { - contractAddress: z.string().describe("The contract address"), - abiJson: z.string().optional().describe("The contract ABI as a JSON string"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + description: "Get guidance on interacting with a smart contract", + argsSchema: { + contractAddress: z.string().describe("The contract address"), + abiJson: z.string().optional().describe("The contract ABI as a JSON string"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + } }, ({ contractAddress, abiJson, network = "ethereum" }) => ({ messages: [{ @@ -80,7 +88,7 @@ export function registerEVMPrompts(server: McpServer) { content: { type: "text", text: abiJson - ? `I need to interact with the smart contract at address ${contractAddress} on the ${network} network. Here's the ABI:\n\n${abiJson}\n\nPlease analyze this contract's functions and provide guidance on how to interact with it safely. Explain what each function does and what parameters it requires.` + ? `I need to interact with the smart contract at address ${contractAddress} on the ${network} network. Here's the ABI:\n\n${abiJson}\n\nPlease analyze this contract's functions and provide guidance on how to interact with it safely.` : `I need to interact with the smart contract at address ${contractAddress} on the ${network} network. Please help me understand what this contract does and how I can interact with it safely.` } }] @@ -88,11 +96,13 @@ export function registerEVMPrompts(server: McpServer) { ); // EVM concept explanation - server.prompt( + server.registerPrompt( "explain_evm_concept", - "Get an explanation of an EVM concept", { - concept: z.string().describe("The EVM concept to explain (e.g., gas, nonce, etc.)") + description: "Get an explanation of an EVM concept", + argsSchema: { + concept: z.string().describe("The EVM concept to explain (e.g., gas, nonce, etc.)") + } }, ({ concept }) => ({ messages: [{ @@ -106,11 +116,13 @@ export function registerEVMPrompts(server: McpServer) { ); // Network comparison - server.prompt( + server.registerPrompt( "compare_networks", - "Compare different EVM-compatible networks", { - networkList: z.string().describe("Comma-separated list of networks to compare (e.g., 'ethereum,optimism,arbitrum')") + description: "Compare different EVM-compatible networks", + argsSchema: { + networkList: z.string().describe("Comma-separated list of networks to compare") + } }, ({ networkList }) => { const networks = networkList.split(',').map(n => n.trim()); @@ -127,24 +139,26 @@ export function registerEVMPrompts(server: McpServer) { ); // Token analysis prompt - server.prompt( + server.registerPrompt( "analyze_token", - "Analyze an ERC20 or NFT token", { - tokenAddress: z.string().describe("Token contract address to analyze"), - tokenType: z.string().optional().describe("Type of token to analyze (erc20, erc721/nft, or auto-detect). Defaults to auto."), - tokenId: z.string().optional().describe("Token ID (required for NFT analysis)"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Analyze an ERC20 or NFT token", + argsSchema: { + tokenAddress: z.string().describe("Token contract address to analyze"), + tokenType: z.string().optional().describe("Type of token (erc20, erc721/nft, or auto-detect)"), + tokenId: z.string().optional().describe("Token ID (required for NFT analysis)"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + } }, ({ tokenAddress, tokenType = "auto", tokenId, network = "ethereum" }) => { let promptText = ""; - + if (tokenType === "erc20" || tokenType === "auto") { - promptText = `Please analyze the ERC20 token at address ${tokenAddress} on the ${network} network. Provide information about its name, symbol, total supply, and any other relevant details. If possible, explain the token's purpose, utility, and market context.`; + promptText = `Please analyze the ERC20 token at address ${tokenAddress} on the ${network} network. Provide information about its name, symbol, total supply, and any other relevant details.`; } else if ((tokenType === "erc721" || tokenType === "nft") && tokenId) { - promptText = `Please analyze the NFT with token ID ${tokenId} from the collection at address ${tokenAddress} on the ${network} network. Provide information about the collection name, token details, ownership history if available, and any other relevant information about this specific NFT.`; + promptText = `Please analyze the NFT with token ID ${tokenId} from the collection at address ${tokenAddress} on the ${network} network. Provide information about the collection name, token details, and any other relevant information.`; } else if (tokenType === "nft" || tokenType === "erc721") { - promptText = `Please analyze the NFT collection at address ${tokenAddress} on the ${network} network. Provide information about the collection name, symbol, total supply if available, floor price if available, and any other relevant details about this NFT collection.`; + promptText = `Please analyze the NFT collection at address ${tokenAddress} on the ${network} network. Provide information about the collection name, symbol, total supply if available, and any other relevant details.`; } return { @@ -158,5 +172,4 @@ export function registerEVMPrompts(server: McpServer) { }; } ); - -} \ No newline at end of file +} diff --git a/src/core/resources.ts b/src/core/resources.ts index f8afca9..7ef0879 100644 --- a/src/core/resources.ts +++ b/src/core/resources.ts @@ -8,629 +8,393 @@ import type { Address, Hash } from "viem"; * @param server The MCP server instance */ export function registerEVMResources(server: McpServer) { - // Get EVM info for a specific network - server.resource( - "chain_info_by_network", + // ============================================================================ + // CHAIN INFO RESOURCES + // ============================================================================ + + server.registerResource( + "chain_info_by_network", new ResourceTemplate("evm://{network}/chain", { list: undefined }), + { description: "Get chain information for a specific network", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const chainId = await services.getChainId(network); const blockNumber = await services.getBlockNumber(network); const rpcUrl = getRpcUrl(network); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - network, - chainId, - blockNumber: blockNumber.toString(), - rpcUrl - }, null, 2) - }] - }; + return { contents: [{ uri: uri.href, text: JSON.stringify({ network, chainId, blockNumber: blockNumber.toString(), rpcUrl }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Default chain info (Ethereum mainnet) - server.resource( - "ethereum_chain_info", + server.registerResource( + "ethereum_chain_info", "evm://chain", + { description: "Get Ethereum mainnet chain information", mimeType: "application/json" }, async (uri) => { try { const network = "ethereum"; const chainId = await services.getChainId(network); const blockNumber = await services.getBlockNumber(network); const rpcUrl = getRpcUrl(network); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - network, - chainId, - blockNumber: blockNumber.toString(), - rpcUrl - }, null, 2) - }] - }; + return { contents: [{ uri: uri.href, text: JSON.stringify({ network, chainId, blockNumber: blockNumber.toString(), rpcUrl }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Get block by number for a specific network - server.resource( + server.registerResource( + "supported_networks", + "evm://networks", + { description: "Get list of all supported EVM networks", mimeType: "application/json" }, + async (uri) => { + try { + const networks = getSupportedNetworks(); + return { contents: [{ uri: uri.href, text: JSON.stringify({ supportedNetworks: networks }, null, 2) }] }; + } catch (error) { + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; + } + } + ); + + // ============================================================================ + // BLOCK RESOURCES + // ============================================================================ + + server.registerResource( "evm_block_by_number", new ResourceTemplate("evm://{network}/block/{blockNumber}", { list: undefined }), + { description: "Get block by number for a specific network", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const blockNumber = params.blockNumber as string; const block = await services.getBlockByNumber(parseInt(blockNumber), network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(block) - }] - }; + return { contents: [{ uri: uri.href, text: services.helpers.formatJson(block) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching block: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Get block by hash for a specific network - server.resource( + server.registerResource( "block_by_hash", new ResourceTemplate("evm://{network}/block/hash/{blockHash}", { list: undefined }), + { description: "Get block by hash for a specific network", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const blockHash = params.blockHash as string; const block = await services.getBlockByHash(blockHash as Hash, network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(block) - }] - }; + return { contents: [{ uri: uri.href, text: services.helpers.formatJson(block) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching block with hash: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Get latest block for a specific network - server.resource( + server.registerResource( "evm_latest_block", new ResourceTemplate("evm://{network}/block/latest", { list: undefined }), + { description: "Get latest block for a specific network", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const block = await services.getLatestBlock(network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(block) - }] - }; + return { contents: [{ uri: uri.href, text: services.helpers.formatJson(block) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Default latest block (Ethereum mainnet) - server.resource( + server.registerResource( "default_latest_block", "evm://block/latest", + { description: "Get latest Ethereum mainnet block", mimeType: "application/json" }, async (uri) => { try { - const network = "ethereum"; - const block = await services.getLatestBlock(network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(block) - }] - }; + const block = await services.getLatestBlock("ethereum"); + return { contents: [{ uri: uri.href, text: services.helpers.formatJson(block) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Get ETH balance for a specific network - server.resource( + // ============================================================================ + // BALANCE RESOURCES + // ============================================================================ + + server.registerResource( "evm_address_native_balance", new ResourceTemplate("evm://{network}/address/{address}/balance", { list: undefined }), + { description: "Get native token balance for an address on a specific network", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const address = params.address as string; const balance = await services.getETHBalance(address as Address, network); - return { contents: [{ uri: uri.href, - text: JSON.stringify({ - network, - address, - balance: { - wei: balance.wei.toString(), - ether: balance.ether - } - }, null, 2) + text: JSON.stringify({ network, address, balance: { wei: balance.wei.toString(), ether: balance.ether } }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ETH balance: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Default ETH balance (Ethereum mainnet) - server.resource( + server.registerResource( "default_eth_balance", new ResourceTemplate("evm://address/{address}/eth-balance", { list: undefined }), + { description: "Get ETH balance on Ethereum mainnet", mimeType: "application/json" }, async (uri, params) => { try { - const network = "ethereum"; const address = params.address as string; - const balance = await services.getETHBalance(address as Address, network); - + const balance = await services.getETHBalance(address as Address, "ethereum"); return { contents: [{ uri: uri.href, - text: JSON.stringify({ - network, - address, - balance: { - wei: balance.wei.toString(), - ether: balance.ether - } - }, null, 2) + text: JSON.stringify({ network: "ethereum", address, balance: { wei: balance.wei.toString(), ether: balance.ether } }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ETH balance: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Get ERC20 balance for a specific network - server.resource( + server.registerResource( "erc20_balance", new ResourceTemplate("evm://{network}/address/{address}/token/{tokenAddress}/balance", { list: undefined }), + { description: "Get ERC20 token balance for an address", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const address = params.address as string; const tokenAddress = params.tokenAddress as string; - - const balance = await services.getERC20Balance( - tokenAddress as Address, - address as Address, - network - ); - + const balance = await services.getERC20Balance(tokenAddress as Address, address as Address, network); return { contents: [{ uri: uri.href, text: JSON.stringify({ - network, - address, - tokenAddress, - balance: { - raw: balance.raw.toString(), - formatted: balance.formatted, - decimals: balance.token.decimals - } + network, address, tokenAddress, + balance: { raw: balance.raw.toString(), formatted: balance.formatted, decimals: balance.token.decimals } }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ERC20 balance: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Default ERC20 balance (Ethereum mainnet) - server.resource( + server.registerResource( "default_erc20_balance", new ResourceTemplate("evm://address/{address}/token/{tokenAddress}/balance", { list: undefined }), + { description: "Get ERC20 token balance on Ethereum mainnet", mimeType: "application/json" }, async (uri, params) => { try { - const network = "ethereum"; const address = params.address as string; const tokenAddress = params.tokenAddress as string; - - const balance = await services.getERC20Balance( - tokenAddress as Address, - address as Address, - network - ); - + const balance = await services.getERC20Balance(tokenAddress as Address, address as Address, "ethereum"); return { contents: [{ uri: uri.href, text: JSON.stringify({ - network, - address, - tokenAddress, - balance: { - raw: balance.raw.toString(), - formatted: balance.formatted, - decimals: balance.token.decimals - } + network: "ethereum", address, tokenAddress, + balance: { raw: balance.raw.toString(), formatted: balance.formatted, decimals: balance.token.decimals } }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ERC20 balance: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Get transaction by hash for a specific network - server.resource( + // ============================================================================ + // TRANSACTION RESOURCES + // ============================================================================ + + server.registerResource( "evm_transaction_details", new ResourceTemplate("evm://{network}/tx/{txHash}", { list: undefined }), + { description: "Get transaction details by hash", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const txHash = params.txHash as string; const tx = await services.getTransaction(txHash as Hash, network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(tx) - }] - }; + return { contents: [{ uri: uri.href, text: services.helpers.formatJson(tx) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching transaction: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Default transaction by hash (Ethereum mainnet) - server.resource( + server.registerResource( "default_transaction_by_hash", new ResourceTemplate("evm://tx/{txHash}", { list: undefined }), + { description: "Get Ethereum mainnet transaction by hash", mimeType: "application/json" }, async (uri, params) => { try { - const network = "ethereum"; const txHash = params.txHash as string; - const tx = await services.getTransaction(txHash as Hash, network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(tx) - }] - }; + const tx = await services.getTransaction(txHash as Hash, "ethereum"); + return { contents: [{ uri: uri.href, text: services.helpers.formatJson(tx) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching transaction: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Get supported networks - server.resource( - "supported_networks", - "evm://networks", - async (uri) => { - try { - const networks = getSupportedNetworks(); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - supportedNetworks: networks - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching supported networks: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); + // ============================================================================ + // TOKEN INFO RESOURCES + // ============================================================================ - // Add ERC20 token info resource - server.resource( + server.registerResource( "erc20_token_details", new ResourceTemplate("evm://{network}/token/{tokenAddress}", { list: undefined }), + { description: "Get ERC20 token details", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const tokenAddress = params.tokenAddress as Address; - const tokenInfo = await services.getERC20TokenInfo(tokenAddress, network); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - address: tokenAddress, - network, - ...tokenInfo - }, null, 2) - }] - }; + return { contents: [{ uri: uri.href, text: JSON.stringify({ address: tokenAddress, network, ...tokenInfo }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ERC20 token info: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Add ERC20 token balance resource - server.resource( + server.registerResource( "erc20_token_address_balance", new ResourceTemplate("evm://{network}/token/{tokenAddress}/balanceOf/{address}", { list: undefined }), + { description: "Get ERC20 token balance for an address", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const tokenAddress = params.tokenAddress as Address; const address = params.address as Address; - const balance = await services.getERC20Balance(tokenAddress, address, network); - return { contents: [{ uri: uri.href, text: JSON.stringify({ - tokenAddress, - owner: address, - network, - raw: balance.raw.toString(), - formatted: balance.formatted, - symbol: balance.token.symbol, - decimals: balance.token.decimals + tokenAddress, owner: address, network, + raw: balance.raw.toString(), formatted: balance.formatted, + symbol: balance.token.symbol, decimals: balance.token.decimals }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ERC20 token balance: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Add NFT (ERC721) token info resource - server.resource( + // ============================================================================ + // NFT (ERC721) RESOURCES + // ============================================================================ + + server.registerResource( "erc721_nft_token_details", new ResourceTemplate("evm://{network}/nft/{tokenAddress}/{tokenId}", { list: undefined }), + { description: "Get NFT token details", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const tokenAddress = params.tokenAddress as Address; const tokenId = BigInt(params.tokenId as string); - const nftInfo = await services.getERC721TokenMetadata(tokenAddress, tokenId, network); - - // Get owner separately - let owner = "Unknown"; - try { - const isOwner = await services.isNFTOwner(tokenAddress, params.address as Address, tokenId, network); - if (isOwner) { - owner = params.address as string; - } - } catch (e) { - // Owner info not available - } - return { contents: [{ uri: uri.href, - text: JSON.stringify({ - contract: tokenAddress, - tokenId: tokenId.toString(), - network, - ...nftInfo, - owner - }, null, 2) + text: JSON.stringify({ contract: tokenAddress, tokenId: tokenId.toString(), network, ...nftInfo }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching NFT info: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Add NFT ownership check resource - server.resource( + server.registerResource( "erc721_nft_ownership_check", new ResourceTemplate("evm://{network}/nft/{tokenAddress}/{tokenId}/isOwnedBy/{address}", { list: undefined }), + { description: "Check NFT ownership", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const tokenAddress = params.tokenAddress as Address; const tokenId = BigInt(params.tokenId as string); const address = params.address as Address; - const isOwner = await services.isNFTOwner(tokenAddress, address, tokenId, network); - return { contents: [{ uri: uri.href, - text: JSON.stringify({ - contract: tokenAddress, - tokenId: tokenId.toString(), - owner: address, - network, - isOwner - }, null, 2) + text: JSON.stringify({ contract: tokenAddress, tokenId: tokenId.toString(), owner: address, network, isOwner }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error checking NFT ownership: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Add ERC1155 token URI resource - server.resource( + // ============================================================================ + // ERC1155 RESOURCES + // ============================================================================ + + server.registerResource( "erc1155_token_metadata_uri", new ResourceTemplate("evm://{network}/erc1155/{tokenAddress}/{tokenId}/uri", { list: undefined }), + { description: "Get ERC1155 token metadata URI", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const tokenAddress = params.tokenAddress as Address; const tokenId = BigInt(params.tokenId as string); - const tokenURI = await services.getERC1155TokenURI(tokenAddress, tokenId, network); - return { contents: [{ uri: uri.href, - text: JSON.stringify({ - contract: tokenAddress, - tokenId: tokenId.toString(), - network, - uri: tokenURI - }, null, 2) + text: JSON.stringify({ contract: tokenAddress, tokenId: tokenId.toString(), network, uri: tokenURI }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ERC1155 token URI: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Add ERC1155 token balance resource - server.resource( + server.registerResource( "erc1155_token_address_balance", new ResourceTemplate("evm://{network}/erc1155/{tokenAddress}/{tokenId}/balanceOf/{address}", { list: undefined }), + { description: "Get ERC1155 token balance", mimeType: "application/json" }, async (uri, params) => { try { const network = params.network as string; const tokenAddress = params.tokenAddress as Address; const tokenId = BigInt(params.tokenId as string); const address = params.address as Address; - const balance = await services.getERC1155Balance(tokenAddress, address, tokenId, network); - return { contents: [{ uri: uri.href, - text: JSON.stringify({ - contract: tokenAddress, - tokenId: tokenId.toString(), - owner: address, - network, - balance: balance.toString() - }, null, 2) + text: JSON.stringify({ contract: tokenAddress, tokenId: tokenId.toString(), owner: address, network, balance: balance.toString() }, null, 2) }] }; } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ERC1155 token balance: ${error instanceof Error ? error.message : String(error)}` - }] - }; + return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); -} \ No newline at end of file +} diff --git a/src/core/tools.ts b/src/core/tools.ts index a01d7a7..1b2c213 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -7,1190 +7,1004 @@ import { normalize } from 'viem/ens'; /** * Register all EVM-related tools with the MCP server - * + * * All tools that accept Ethereum addresses also support ENS names (e.g., 'vitalik.eth'). * ENS names are automatically resolved to addresses using the Ethereum Name Service. - * + * * @param server The MCP server instance */ export function registerEVMTools(server: McpServer) { - // NETWORK INFORMATION TOOLS - - // Get chain information - server.tool( + // ============================================================================ + // NETWORK INFORMATION TOOLS (Read-only) + // ============================================================================ + + server.registerTool( "get_chain_info", - "Get information about an EVM network", { - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Get information about an EVM network including chain ID, block number, and RPC URL", + inputSchema: { + network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Chain Info", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, async ({ network = "ethereum" }) => { try { const chainId = await services.getChainId(network); const blockNumber = await services.getBlockNumber(network); const rpcUrl = getRpcUrl(network); - + return { content: [{ type: "text", - text: JSON.stringify({ - network, - chainId, - blockNumber: blockNumber.toString(), - rpcUrl - }, null, 2) + text: JSON.stringify({ network, chainId, blockNumber: blockNumber.toString(), rpcUrl }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // ENS LOOKUP TOOL - - // Resolve ENS name to address - server.tool( - "resolve_ens", - "Resolve an ENS name to an Ethereum address", + server.registerTool( + "get_supported_networks", { - ensName: z.string().describe("ENS name to resolve (e.g., 'vitalik.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. ENS resolution works best on Ethereum mainnet. Defaults to Ethereum mainnet.") + description: "Get a list of all supported EVM networks", + inputSchema: {}, + annotations: { + title: "Get Supported Networks", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }, - async ({ ensName, network = "ethereum" }) => { + async () => { try { - // Validate that the input is an ENS name - if (!ensName.includes('.')) { - return { - content: [{ - type: "text", - text: `Error: Input "${ensName}" is not a valid ENS name. ENS names must contain a dot (e.g., 'name.eth').` - }], - isError: true - }; - } - - // Normalize the ENS name - const normalizedEns = normalize(ensName); - - // Resolve the ENS name to an address - const address = await services.resolveAddress(ensName, network); - + const networks = getSupportedNetworks(); return { - content: [{ - type: "text", - text: JSON.stringify({ - ensName: ensName, - normalizedName: normalizedEns, - resolvedAddress: address, - network - }, null, 2) - }] + content: [{ type: "text", text: JSON.stringify({ supportedNetworks: networks }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error resolving ENS name: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching supported networks: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get supported networks - server.tool( - "get_supported_networks", - "Get a list of supported EVM networks", - {}, - async () => { + server.registerTool( + "resolve_ens", + { + description: "Resolve an ENS name to an Ethereum address", + inputSchema: { + ensName: z.string().describe("ENS name to resolve (e.g., 'vitalik.eth')"), + network: z.string().optional().describe("Network name or chain ID. ENS resolution works best on Ethereum mainnet. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Resolve ENS Name", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } + }, + async ({ ensName, network = "ethereum" }) => { try { - const networks = getSupportedNetworks(); - + if (!ensName.includes('.')) { + return { + content: [{ type: "text", text: `Error: Input "${ensName}" is not a valid ENS name. ENS names must contain a dot (e.g., 'name.eth').` }], + isError: true + }; + } + const normalizedEns = normalize(ensName); + const address = await services.resolveAddress(ensName, network); + return { - content: [{ - type: "text", - text: JSON.stringify({ - supportedNetworks: networks - }, null, 2) - }] + content: [{ type: "text", text: JSON.stringify({ ensName, normalizedName: normalizedEns, resolvedAddress: address, network }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching supported networks: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error resolving ENS name: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // BLOCK TOOLS - - // Get block by number - server.tool( + // ============================================================================ + // BLOCK TOOLS (Read-only) + // ============================================================================ + + server.registerTool( "get_block_by_number", - "Get a block by its block number", { - blockNumber: z.number().describe("The block number to fetch"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + description: "Get a block by its block number", + inputSchema: { + blockNumber: z.number().describe("The block number to fetch"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Block by Number", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, async ({ blockNumber, network = "ethereum" }) => { try { const block = await services.getBlockByNumber(blockNumber, network); - - return { - content: [{ - type: "text", - text: services.helpers.formatJson(block) - }] - }; + return { content: [{ type: "text", text: services.helpers.formatJson(block) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching block ${blockNumber}: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching block ${blockNumber}: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get latest block - server.tool( + server.registerTool( "get_latest_block", - "Get the latest block from the EVM", { - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + description: "Get the latest block from the EVM network", + inputSchema: { + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Latest Block", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true + } }, async ({ network = "ethereum" }) => { try { const block = await services.getLatestBlock(network); - - return { - content: [{ - type: "text", - text: services.helpers.formatJson(block) - }] - }; + return { content: [{ type: "text", text: services.helpers.formatJson(block) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // BALANCE TOOLS - - // Get ETH balance - server.tool( + // ============================================================================ + // BALANCE TOOLS (Read-only) + // ============================================================================ + + server.registerTool( "get_balance", - "Get the native token balance (ETH, MATIC, etc.) for an address", { - address: z.string().describe("The wallet address or ENS name (e.g., '0x1234...' or 'vitalik.eth') to check the balance for"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Get the native token balance (ETH, MATIC, etc.) for an address", + inputSchema: { + address: z.string().describe("The wallet address or ENS name to check the balance for"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Native Balance", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, async ({ address, network = "ethereum" }) => { try { const balance = await services.getETHBalance(address, network); - return { - content: [{ - type: "text", - text: JSON.stringify({ - address, - network, - wei: balance.wei.toString(), - ether: balance.ether - }, null, 2) - }] + content: [{ type: "text", text: JSON.stringify({ address, network, wei: balance.wei.toString(), ether: balance.ether }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching balance: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching balance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get ERC20 balance - server.tool( + server.registerTool( "get_erc20_balance", - "Get the ERC20 token balance of an Ethereum address", { - address: z.string().describe("The Ethereum address to check"), - tokenAddress: z.string().describe("The ERC20 token contract address"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + description: "Get the ERC20 token balance of an address", + inputSchema: { + address: z.string().describe("The address to check"), + tokenAddress: z.string().describe("The ERC20 token contract address"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get ERC20 Balance", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, async ({ address, tokenAddress, network = "ethereum" }) => { try { - const balance = await services.getERC20Balance( - tokenAddress as Address, - address as Address, - network - ); - + const balance = await services.getERC20Balance(tokenAddress as Address, address as Address, network); return { content: [{ type: "text", text: JSON.stringify({ - address, - tokenAddress, - network, - balance: { - raw: balance.raw.toString(), - formatted: balance.formatted, - decimals: balance.token.decimals - } + address, tokenAddress, network, + balance: { raw: balance.raw.toString(), formatted: balance.formatted, decimals: balance.token.decimals } }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching ERC20 balance for ${address}: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching ERC20 balance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get ERC20 token balance - server.tool( + server.registerTool( "get_token_balance", - "Get the balance of an ERC20 token for an address", { - tokenAddress: z.string().describe("The contract address or ENS name of the ERC20 token (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC or 'uniswap.eth')"), - ownerAddress: z.string().describe("The wallet address or ENS name to check the balance for (e.g., '0x1234...' or 'vitalik.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Get the balance of an ERC20 token for an address", + inputSchema: { + tokenAddress: z.string().describe("The contract address of the ERC20 token"), + ownerAddress: z.string().describe("The wallet address or ENS name to check the balance for"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Token Balance", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, async ({ tokenAddress, ownerAddress, network = "ethereum" }) => { try { const balance = await services.getERC20Balance(tokenAddress, ownerAddress, network); - return { content: [{ type: "text", text: JSON.stringify({ - tokenAddress, - owner: ownerAddress, - network, - raw: balance.raw.toString(), - formatted: balance.formatted, - symbol: balance.token.symbol, - decimals: balance.token.decimals + tokenAddress, owner: ownerAddress, network, + raw: balance.raw.toString(), formatted: balance.formatted, + symbol: balance.token.symbol, decimals: balance.token.decimals }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching token balance: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching token balance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // TRANSACTION TOOLS - - // Get transaction by hash - server.tool( - "get_transaction", - "Get detailed information about a specific transaction by its hash. Includes sender, recipient, value, data, and more.", + server.registerTool( + "get_nft_balance", { - txHash: z.string().describe("The transaction hash to look up (e.g., '0x1234...')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + description: "Get the total number of NFTs owned by an address from a specific ERC721 collection", + inputSchema: { + tokenAddress: z.string().describe("The contract address of the NFT collection"), + ownerAddress: z.string().describe("The wallet address to check the NFT balance for"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get NFT Balance", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ txHash, network = "ethereum" }) => { + async ({ tokenAddress, ownerAddress, network = "ethereum" }) => { try { - const tx = await services.getTransaction(txHash as Hash, network); - + const balance = await services.getERC721Balance(tokenAddress as Address, ownerAddress as Address, network); return { - content: [{ - type: "text", - text: services.helpers.formatJson(tx) - }] + content: [{ type: "text", text: JSON.stringify({ collection: tokenAddress, owner: ownerAddress, network, balance: balance.toString() }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching transaction ${txHash}: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching NFT balance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get transaction receipt - server.tool( - "get_transaction_receipt", - "Get a transaction receipt by its hash", + server.registerTool( + "get_erc1155_balance", { - txHash: z.string().describe("The transaction hash to look up"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + description: "Get the balance of a specific ERC1155 token ID owned by an address", + inputSchema: { + tokenAddress: z.string().describe("The contract address of the ERC1155 token collection"), + tokenId: z.string().describe("The ID of the specific token to check"), + ownerAddress: z.string().describe("The wallet address to check the token balance for"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get ERC1155 Balance", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ txHash, network = "ethereum" }) => { + async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { try { - const receipt = await services.getTransactionReceipt(txHash as Hash, network); - + const balance = await services.getERC1155Balance(tokenAddress as Address, ownerAddress as Address, BigInt(tokenId), network); return { - content: [{ - type: "text", - text: services.helpers.formatJson(receipt) - }] + content: [{ type: "text", text: JSON.stringify({ contract: tokenAddress, tokenId, owner: ownerAddress, network, balance: balance.toString() }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching transaction receipt ${txHash}: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching ERC1155 balance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Estimate gas - server.tool( - "estimate_gas", - "Estimate the gas cost for a transaction", + // ============================================================================ + // TRANSACTION TOOLS (Read-only) + // ============================================================================ + + server.registerTool( + "get_transaction", { - to: z.string().describe("The recipient address"), - value: z.string().optional().describe("The amount of ETH to send in ether (e.g., '0.1')"), - data: z.string().optional().describe("The transaction data as a hex string"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + description: "Get detailed information about a specific transaction by its hash", + inputSchema: { + txHash: z.string().describe("The transaction hash to look up"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Transaction", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ to, value, data, network = "ethereum" }) => { + async ({ txHash, network = "ethereum" }) => { try { - const params: any = { to: to as Address }; - - if (value) { - params.value = services.helpers.parseEther(value); - } - - if (data) { - params.data = data as `0x${string}`; - } - - const gas = await services.estimateGas(params, network); - - return { - content: [{ - type: "text", - text: JSON.stringify({ - network, - estimatedGas: gas.toString() - }, null, 2) - }] - }; + const tx = await services.getTransaction(txHash as Hash, network); + return { content: [{ type: "text", text: services.helpers.formatJson(tx) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error estimating gas: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching transaction: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // TRANSFER TOOLS - - // Transfer ETH - server.tool( - "transfer_eth", - "Transfer native tokens (ETH, MATIC, etc.) to an address", + server.registerTool( + "get_transaction_receipt", { - privateKey: z.string().describe("Private key of the sender account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - to: z.string().describe("The recipient address or ENS name (e.g., '0x1234...' or 'vitalik.eth')"), - amount: z.string().describe("Amount to send in ETH (or the native token of the network), as a string (e.g., '0.1')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Get a transaction receipt by its hash", + inputSchema: { + txHash: z.string().describe("The transaction hash to look up"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Transaction Receipt", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ privateKey, to, amount, network = "ethereum" }) => { + async ({ txHash, network = "ethereum" }) => { try { - const txHash = await services.transferETH(privateKey, to, amount, network); - - return { - content: [{ - type: "text", - text: JSON.stringify({ - success: true, - txHash, - to, - amount, - network - }, null, 2) - }] - }; + const receipt = await services.getTransactionReceipt(txHash as Hash, network); + return { content: [{ type: "text", text: services.helpers.formatJson(receipt) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error transferring ETH: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching receipt: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Transfer ERC20 - server.tool( - "transfer_erc20", - "Transfer ERC20 tokens to another address", + server.registerTool( + "estimate_gas", { - privateKey: z.string().describe("Private key of the sending account (this is used for signing and is never stored)"), - tokenAddress: z.string().describe("The address of the ERC20 token contract"), - toAddress: z.string().describe("The recipient address"), - amount: z.string().describe("The amount of tokens to send (in token units, e.g., '10' for 10 tokens)"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Estimate the gas cost for a transaction", + inputSchema: { + to: z.string().describe("The recipient address"), + value: z.string().optional().describe("The amount of ETH to send in ether (e.g., '0.1')"), + data: z.string().optional().describe("The transaction data as a hex string"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Estimate Gas", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { + async ({ to, value, data, network = "ethereum" }) => { try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - - const result = await services.transferERC20( - tokenAddress as Address, - toAddress as Address, - amount, - formattedKey, - network - ); - + const params: any = { to: to as Address }; + if (value) params.value = services.helpers.parseEther(value); + if (data) params.data = data as `0x${string}`; + + const gas = await services.estimateGas(params, network); return { - content: [{ - type: "text", - text: JSON.stringify({ - success: true, - txHash: result.txHash, - network, - tokenAddress, - recipient: toAddress, - amount: result.amount.formatted, - symbol: result.token.symbol - }, null, 2) - }] + content: [{ type: "text", text: JSON.stringify({ network, estimatedGas: gas.toString() }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error transferring ERC20 tokens: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error estimating gas: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Approve ERC20 token spending - server.tool( - "approve_token_spending", - "Approve another address (like a DeFi protocol or exchange) to spend your ERC20 tokens. This is often required before interacting with DeFi protocols.", + // ============================================================================ + // TOKEN INFO TOOLS (Read-only) + // ============================================================================ + + server.registerTool( + "get_token_info", { - privateKey: z.string().describe("Private key of the token owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - tokenAddress: z.string().describe("The contract address of the ERC20 token to approve for spending (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC on Ethereum)"), - spenderAddress: z.string().describe("The contract address being approved to spend your tokens (e.g., a DEX or lending protocol)"), - amount: z.string().describe("The amount of tokens to approve in token units, not wei (e.g., '1000' to approve spending 1000 tokens). Use a very large number for unlimited approval."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + description: "Get comprehensive information about an ERC20 token including name, symbol, decimals, and total supply", + inputSchema: { + tokenAddress: z.string().describe("The contract address of the ERC20 token"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Token Info", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ privateKey, tokenAddress, spenderAddress, amount, network = "ethereum" }) => { + async ({ tokenAddress, network = "ethereum" }) => { try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - - const result = await services.approveERC20( - tokenAddress as Address, - spenderAddress as Address, - amount, - formattedKey, - network - ); - + const tokenInfo = await services.getERC20TokenInfo(tokenAddress as Address, network); return { - content: [{ - type: "text", - text: JSON.stringify({ - success: true, - txHash: result.txHash, - network, - tokenAddress, - spender: spenderAddress, - amount: result.amount.formatted, - symbol: result.token.symbol - }, null, 2) - }] + content: [{ type: "text", text: JSON.stringify({ address: tokenAddress, network, ...tokenInfo }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error approving token spending: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching token info: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Transfer NFT (ERC721) - server.tool( - "transfer_nft", - "Transfer an NFT (ERC721 token) from one address to another. Requires the private key of the current owner for signing the transaction.", + server.registerTool( + "get_nft_info", { - privateKey: z.string().describe("Private key of the NFT owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), - tokenId: z.string().describe("The ID of the specific NFT to transfer (e.g., '1234')"), - toAddress: z.string().describe("The recipient wallet address that will receive the NFT"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") + description: "Get detailed information about a specific NFT (ERC721 token)", + inputSchema: { + tokenAddress: z.string().describe("The contract address of the NFT collection"), + tokenId: z.string().describe("The ID of the specific NFT token to query"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get NFT Info", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ privateKey, tokenAddress, tokenId, toAddress, network = "ethereum" }) => { + async ({ tokenAddress, tokenId, network = "ethereum" }) => { try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - - const result = await services.transferERC721( - tokenAddress as Address, - toAddress as Address, - BigInt(tokenId), - formattedKey, - network - ); - + const nftInfo = await services.getERC721TokenMetadata(tokenAddress as Address, BigInt(tokenId), network); + let owner = null; + try { + owner = await services.getPublicClient(network).readContract({ + address: tokenAddress as Address, + abi: [{ inputs: [{ type: 'uint256' }], name: 'ownerOf', outputs: [{ type: 'address' }], stateMutability: 'view', type: 'function' }], + functionName: 'ownerOf', + args: [BigInt(tokenId)] + }); + } catch (e) { /* Ownership info not available */ } + return { - content: [{ - type: "text", - text: JSON.stringify({ - success: true, - txHash: result.txHash, - network, - collection: tokenAddress, - tokenId: result.tokenId, - recipient: toAddress, - name: result.token.name, - symbol: result.token.symbol - }, null, 2) - }] + content: [{ type: "text", text: JSON.stringify({ contract: tokenAddress, tokenId, network, ...nftInfo, owner: owner || 'Unknown' }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error transferring NFT: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching NFT info: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Transfer ERC1155 token - server.tool( - "transfer_erc1155", - "Transfer ERC1155 tokens to another address. ERC1155 is a multi-token standard that can represent both fungible and non-fungible tokens in a single contract.", + server.registerTool( + "get_erc1155_token_uri", { - privateKey: z.string().describe("Private key of the token owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), - tokenId: z.string().describe("The ID of the specific token to transfer (e.g., '1234')"), - amount: z.string().describe("The quantity of tokens to send (e.g., '1' for a single NFT or '10' for 10 fungible tokens)"), - toAddress: z.string().describe("The recipient wallet address that will receive the tokens"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") + description: "Get the metadata URI for an ERC1155 token", + inputSchema: { + tokenAddress: z.string().describe("The contract address of the ERC1155 token collection"), + tokenId: z.string().describe("The ID of the specific token to query"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get ERC1155 Token URI", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ privateKey, tokenAddress, tokenId, amount, toAddress, network = "ethereum" }) => { + async ({ tokenAddress, tokenId, network = "ethereum" }) => { try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - - const result = await services.transferERC1155( - tokenAddress as Address, - toAddress as Address, - BigInt(tokenId), - amount, - formattedKey, - network - ); - + const uri = await services.getERC1155TokenURI(tokenAddress as Address, BigInt(tokenId), network); return { - content: [{ - type: "text", - text: JSON.stringify({ - success: true, - txHash: result.txHash, - network, - contract: tokenAddress, - tokenId: result.tokenId, - amount: result.amount, - recipient: toAddress - }, null, 2) - }] + content: [{ type: "text", text: JSON.stringify({ contract: tokenAddress, tokenId, network, uri }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error transferring ERC1155 tokens: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching ERC1155 token URI: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Transfer ERC20 tokens - server.tool( - "transfer_token", - "Transfer ERC20 tokens to an address", + server.registerTool( + "check_nft_ownership", { - privateKey: z.string().describe("Private key of the sender account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - tokenAddress: z.string().describe("The contract address or ENS name of the ERC20 token to transfer (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC or 'uniswap.eth')"), - toAddress: z.string().describe("The recipient address or ENS name that will receive the tokens (e.g., '0x1234...' or 'vitalik.eth')"), - amount: z.string().describe("Amount of tokens to send as a string (e.g., '100' for 100 tokens). This will be adjusted for the token's decimals."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Check if an address owns a specific NFT", + inputSchema: { + tokenAddress: z.string().describe("The contract address of the NFT collection"), + tokenId: z.string().describe("The ID of the NFT to check"), + ownerAddress: z.string().describe("The wallet address to check ownership against"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Check NFT Ownership", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { + async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { try { - const result = await services.transferERC20( - tokenAddress, - toAddress, - amount, - privateKey, - network - ); - + const isOwner = await services.isNFTOwner(tokenAddress, ownerAddress, BigInt(tokenId), network); return { content: [{ type: "text", text: JSON.stringify({ - success: true, - txHash: result.txHash, - tokenAddress, - toAddress, - amount: result.amount.formatted, - symbol: result.token.symbol, - network + tokenAddress, tokenId, ownerAddress, network, isOwner, + result: isOwner ? "Address owns this NFT" : "Address does not own this NFT" }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error transferring tokens: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error checking NFT ownership: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // CONTRACT TOOLS - - // Read contract - server.tool( + // ============================================================================ + // CONTRACT TOOLS (Read-only) + // ============================================================================ + + server.registerTool( "read_contract", - "Read data from a smart contract by calling a view/pure function. This doesn't modify blockchain state and doesn't require gas or signing.", { - contractAddress: z.string().describe("The address of the smart contract to interact with"), - abi: z.array(z.any()).describe("The ABI (Application Binary Interface) of the smart contract function, as a JSON array"), - functionName: z.string().describe("The name of the function to call on the contract (e.g., 'balanceOf')"), - args: z.array(z.any()).optional().describe("The arguments to pass to the function, as an array (e.g., ['0x1234...'])"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + description: "Read data from a smart contract by calling a view/pure function. This doesn't modify blockchain state.", + inputSchema: { + contractAddress: z.string().describe("The address of the smart contract"), + abi: z.array(z.any()).describe("The ABI of the smart contract function, as a JSON array"), + functionName: z.string().describe("The name of the function to call"), + args: z.array(z.any()).optional().describe("The arguments to pass to the function"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Read Contract", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, async ({ contractAddress, abi, functionName, args = [], network = "ethereum" }) => { try { - // Parse ABI if it's a string const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi; - const params = { address: contractAddress as Address, abi: parsedAbi, - functionName, - args - }; - - const result = await services.readContract(params, network); - - return { - content: [{ - type: "text", - text: services.helpers.formatJson(result) - }] - }; - } catch (error) { - return { - content: [{ - type: "text", - text: `Error reading contract: ${error instanceof Error ? error.message : String(error)}` - }], - isError: true - }; - } - } - ); - - // Write to contract - server.tool( - "write_contract", - "Write data to a smart contract by calling a state-changing function. This modifies blockchain state and requires gas payment and transaction signing.", - { - contractAddress: z.string().describe("The address of the smart contract to interact with"), - abi: z.array(z.any()).describe("The ABI (Application Binary Interface) of the smart contract function, as a JSON array"), - functionName: z.string().describe("The name of the function to call on the contract (e.g., 'transfer')"), - args: z.array(z.any()).describe("The arguments to pass to the function, as an array (e.g., ['0x1234...', '1000000000000000000'])"), - privateKey: z.string().describe("Private key of the sending account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") - }, - async ({ contractAddress, abi, functionName, args, privateKey, network = "ethereum" }) => { - try { - // Parse ABI if it's a string - const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi; - - const contractParams: Record = { - address: contractAddress as Address, - abi: parsedAbi, - functionName, - args - }; - - const txHash = await services.writeContract( - privateKey as Hex, - contractParams, - network - ); - - return { - content: [{ - type: "text", - text: JSON.stringify({ - network, - transactionHash: txHash, - message: "Contract write transaction sent successfully" - }, null, 2) - }] + functionName: functionName as string, + args: args as readonly unknown[] }; + const result = await services.readContract(params, network as string); + return { content: [{ type: "text", text: services.helpers.formatJson(result) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error writing to contract: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error reading contract: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Check if address is a contract - server.tool( + server.registerTool( "is_contract", - "Check if an address is a smart contract or an externally owned account (EOA)", { - address: z.string().describe("The wallet or contract address or ENS name to check (e.g., '0x1234...' or 'uniswap.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Check if an address is a smart contract or an externally owned account (EOA)", + inputSchema: { + address: z.string().describe("The address to check"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Is Contract", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, async ({ address, network = "ethereum" }) => { try { const isContract = await services.isContract(address, network); - return { content: [{ type: "text", text: JSON.stringify({ - address, - network, - isContract, + address, network, isContract, type: isContract ? "Contract" : "Externally Owned Account (EOA)" }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error checking if address is a contract: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error checking if address is a contract: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get ERC20 token information - server.tool( - "get_token_info", - "Get comprehensive information about an ERC20 token including name, symbol, decimals, total supply, and other metadata. Use this to analyze any token on EVM chains.", + // ============================================================================ + // WALLET TOOLS (Read-only) + // ============================================================================ + + server.registerTool( + "get_address_from_private_key", { - tokenAddress: z.string().describe("The contract address of the ERC20 token (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC on Ethereum)"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + description: "Get the EVM address derived from a private key", + inputSchema: { + privateKey: z.string().describe("Private key in hex format. SECURITY: Used only for address derivation and is not stored.") + }, + annotations: { + title: "Get Address from Private Key", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }, - async ({ tokenAddress, network = "ethereum" }) => { + async ({ privateKey }) => { try { - const tokenInfo = await services.getERC20TokenInfo(tokenAddress as Address, network); - + const formattedKey = privateKey.startsWith('0x') ? privateKey as Hex : `0x${privateKey}` as Hex; + const address = services.getAddressFromPrivateKey(formattedKey); return { - content: [{ - type: "text", - text: JSON.stringify({ - address: tokenAddress, - network, - ...tokenInfo - }, null, 2) - }] + content: [{ type: "text", text: JSON.stringify({ address }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching token info: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error deriving address: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get ERC20 token balance - server.tool( - "get_token_balance_erc20", - "Get ERC20 token balance for an address", + // ============================================================================ + // TRANSFER TOOLS (Destructive - modify blockchain state) + // ============================================================================ + + server.registerTool( + "transfer_eth", { - address: z.string().describe("The address to check balance for"), - tokenAddress: z.string().describe("The ERC20 token contract address"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + description: "Transfer native tokens (ETH, MATIC, etc.) to an address. This modifies blockchain state.", + inputSchema: { + privateKey: z.string().describe("Private key of the sender. SECURITY: Used only for signing and is not stored."), + to: z.string().describe("The recipient address or ENS name"), + amount: z.string().describe("Amount to send in ether (e.g., '0.1')"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Transfer Native Tokens", + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true + } }, - async ({ address, tokenAddress, network = "ethereum" }) => { + async ({ privateKey, to, amount, network = "ethereum" }) => { try { - const balance = await services.getERC20Balance( - tokenAddress as Address, - address as Address, - network - ); - + const txHash = await services.transferETH(privateKey, to, amount, network); return { - content: [{ - type: "text", - text: JSON.stringify({ - address, - tokenAddress, - network, - balance: { - raw: balance.raw.toString(), - formatted: balance.formatted, - decimals: balance.token.decimals - } - }, null, 2) - }] + content: [{ type: "text", text: JSON.stringify({ success: true, txHash, to, amount, network }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching ERC20 balance for ${address}: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error transferring: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get NFT (ERC721) information - server.tool( - "get_nft_info", - "Get detailed information about a specific NFT (ERC721 token), including collection name, symbol, token URI, and current owner if available.", + server.registerTool( + "transfer_erc20", { - tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), - tokenId: z.string().describe("The ID of the specific NFT token to query (e.g., '1234')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") + description: "Transfer ERC20 tokens to another address. This modifies blockchain state.", + inputSchema: { + privateKey: z.string().describe("Private key of the sender. SECURITY: Used only for signing and is not stored."), + tokenAddress: z.string().describe("The ERC20 token contract address"), + toAddress: z.string().describe("The recipient address"), + amount: z.string().describe("Amount of tokens to send (e.g., '10' for 10 tokens)"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Transfer ERC20 Tokens", + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true + } }, - async ({ tokenAddress, tokenId, network = "ethereum" }) => { + async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { try { - const nftInfo = await services.getERC721TokenMetadata( - tokenAddress as Address, - BigInt(tokenId), - network - ); - - // Check ownership separately - let owner = null; - try { - // This may fail if tokenId doesn't exist - owner = await services.getPublicClient(network).readContract({ - address: tokenAddress as Address, - abi: [{ - inputs: [{ type: 'uint256' }], - name: 'ownerOf', - outputs: [{ type: 'address' }], - stateMutability: 'view', - type: 'function' - }], - functionName: 'ownerOf', - args: [BigInt(tokenId)] - }); - } catch (e) { - // Ownership info not available - } - + const formattedKey = privateKey.startsWith('0x') ? privateKey as `0x${string}` : `0x${privateKey}` as `0x${string}`; + const result = await services.transferERC20(tokenAddress as Address, toAddress as Address, amount, formattedKey, network); return { content: [{ type: "text", text: JSON.stringify({ - contract: tokenAddress, - tokenId, - network, - ...nftInfo, - owner: owner || 'Unknown' + success: true, txHash: result.txHash, network, tokenAddress, + recipient: toAddress, amount: result.amount.formatted, symbol: result.token.symbol }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching NFT info: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error transferring ERC20: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Check NFT ownership - server.tool( - "check_nft_ownership", - "Check if an address owns a specific NFT", + server.registerTool( + "transfer_token", { - tokenAddress: z.string().describe("The contract address or ENS name of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for BAYC or 'boredapeyachtclub.eth')"), - tokenId: z.string().describe("The ID of the NFT to check (e.g., '1234')"), - ownerAddress: z.string().describe("The wallet address or ENS name to check ownership against (e.g., '0x1234...' or 'vitalik.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Transfer ERC20 tokens to an address (alias for transfer_erc20)", + inputSchema: { + privateKey: z.string().describe("Private key of the sender. SECURITY: Used only for signing."), + tokenAddress: z.string().describe("The ERC20 token contract address"), + toAddress: z.string().describe("The recipient address or ENS name"), + amount: z.string().describe("Amount of tokens to send"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Transfer Token", + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true + } }, - async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { + async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { try { - const isOwner = await services.isNFTOwner( - tokenAddress, - ownerAddress, - BigInt(tokenId), - network - ); - + const result = await services.transferERC20(tokenAddress, toAddress, amount, privateKey, network); return { content: [{ type: "text", text: JSON.stringify({ - tokenAddress, - tokenId, - ownerAddress, - network, - isOwner, - result: isOwner ? "Address owns this NFT" : "Address does not own this NFT" + success: true, txHash: result.txHash, tokenAddress, toAddress, + amount: result.amount.formatted, symbol: result.token.symbol, network }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error checking NFT ownership: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error transferring tokens: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Add tool for getting ERC1155 token URI - server.tool( - "get_erc1155_token_uri", - "Get the metadata URI for an ERC1155 token (multi-token standard used for both fungible and non-fungible tokens). The URI typically points to JSON metadata about the token.", + server.registerTool( + "transfer_nft", { - tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), - tokenId: z.string().describe("The ID of the specific token to query metadata for (e.g., '1234')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") + description: "Transfer an NFT (ERC721 token) to another address. This modifies blockchain state.", + inputSchema: { + privateKey: z.string().describe("Private key of the NFT owner. SECURITY: Used only for signing."), + tokenAddress: z.string().describe("The NFT collection contract address"), + tokenId: z.string().describe("The ID of the NFT to transfer"), + toAddress: z.string().describe("The recipient address"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Transfer NFT", + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true + } }, - async ({ tokenAddress, tokenId, network = "ethereum" }) => { + async ({ privateKey, tokenAddress, tokenId, toAddress, network = "ethereum" }) => { try { - const uri = await services.getERC1155TokenURI( - tokenAddress as Address, - BigInt(tokenId), - network - ); - + const formattedKey = privateKey.startsWith('0x') ? privateKey as `0x${string}` : `0x${privateKey}` as `0x${string}`; + const result = await services.transferERC721(tokenAddress as Address, toAddress as Address, BigInt(tokenId), formattedKey, network); return { content: [{ type: "text", text: JSON.stringify({ - contract: tokenAddress, - tokenId, - network, - uri + success: true, txHash: result.txHash, network, collection: tokenAddress, + tokenId: result.tokenId, recipient: toAddress, name: result.token.name, symbol: result.token.symbol }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching ERC1155 token URI: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error transferring NFT: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Add tool for getting ERC721 NFT balance - server.tool( - "get_nft_balance", - "Get the total number of NFTs owned by an address from a specific collection. This returns the count of NFTs, not individual token IDs.", + server.registerTool( + "transfer_erc1155", { - tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), - ownerAddress: z.string().describe("The wallet address to check the NFT balance for (e.g., '0x1234...')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") + description: "Transfer ERC1155 tokens to another address. This modifies blockchain state.", + inputSchema: { + privateKey: z.string().describe("Private key of the token owner. SECURITY: Used only for signing."), + tokenAddress: z.string().describe("The ERC1155 token collection contract address"), + tokenId: z.string().describe("The ID of the token to transfer"), + amount: z.string().describe("The quantity of tokens to send"), + toAddress: z.string().describe("The recipient address"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Transfer ERC1155 Tokens", + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true + } }, - async ({ tokenAddress, ownerAddress, network = "ethereum" }) => { + async ({ privateKey, tokenAddress, tokenId, amount, toAddress, network = "ethereum" }) => { try { - const balance = await services.getERC721Balance( - tokenAddress as Address, - ownerAddress as Address, - network - ); - + const formattedKey = privateKey.startsWith('0x') ? privateKey as `0x${string}` : `0x${privateKey}` as `0x${string}`; + const result = await services.transferERC1155(tokenAddress as Address, toAddress as Address, BigInt(tokenId), amount, formattedKey, network); return { content: [{ type: "text", text: JSON.stringify({ - collection: tokenAddress, - owner: ownerAddress, - network, - balance: balance.toString() + success: true, txHash: result.txHash, network, contract: tokenAddress, + tokenId: result.tokenId, amount: result.amount, recipient: toAddress }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching NFT balance: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error transferring ERC1155: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Add tool for getting ERC1155 token balance - server.tool( - "get_erc1155_balance", - "Get the balance of a specific ERC1155 token ID owned by an address. ERC1155 allows multiple tokens of the same ID, so the balance can be greater than 1.", + server.registerTool( + "approve_token_spending", { - tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), - tokenId: z.string().describe("The ID of the specific token to check the balance for (e.g., '1234')"), - ownerAddress: z.string().describe("The wallet address to check the token balance for (e.g., '0x1234...')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") + description: "Approve another address to spend your ERC20 tokens. This modifies blockchain state.", + inputSchema: { + privateKey: z.string().describe("Private key of the token owner. SECURITY: Used only for signing."), + tokenAddress: z.string().describe("The ERC20 token contract address"), + spenderAddress: z.string().describe("The address being approved to spend tokens"), + amount: z.string().describe("The amount of tokens to approve"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Approve Token Spending", + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true + } }, - async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { + async ({ privateKey, tokenAddress, spenderAddress, amount, network = "ethereum" }) => { try { - const balance = await services.getERC1155Balance( - tokenAddress as Address, - ownerAddress as Address, - BigInt(tokenId), - network - ); - + const formattedKey = privateKey.startsWith('0x') ? privateKey as `0x${string}` : `0x${privateKey}` as `0x${string}`; + const result = await services.approveERC20(tokenAddress as Address, spenderAddress as Address, amount, formattedKey, network); return { content: [{ type: "text", text: JSON.stringify({ - contract: tokenAddress, - tokenId, - owner: ownerAddress, - network, - balance: balance.toString() + success: true, txHash: result.txHash, network, tokenAddress, + spender: spenderAddress, amount: result.amount.formatted, symbol: result.token.symbol }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching ERC1155 token balance: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error approving: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // WALLET TOOLS - - // Get address from private key - server.tool( - "get_address_from_private_key", - "Get the EVM address derived from a private key", + server.registerTool( + "write_contract", { - privateKey: z.string().describe("Private key in hex format (with or without 0x prefix). SECURITY: This is used only for address derivation and is not stored.") + description: "Write data to a smart contract by calling a state-changing function. This modifies blockchain state.", + inputSchema: { + contractAddress: z.string().describe("The smart contract address"), + abi: z.array(z.any()).describe("The ABI of the function, as a JSON array"), + functionName: z.string().describe("The name of the function to call"), + args: z.array(z.any()).describe("The arguments to pass to the function"), + privateKey: z.string().describe("Private key of the sender. SECURITY: Used only for signing."), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Write to Smart Contract", + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true + } }, - async ({ privateKey }) => { + async ({ contractAddress, abi, functionName, args, privateKey, network = "ethereum" }) => { try { - // Ensure the private key has 0x prefix - const formattedKey = privateKey.startsWith('0x') ? privateKey as Hex : `0x${privateKey}` as Hex; - - const address = services.getAddressFromPrivateKey(formattedKey); - + const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi; + const contractParams: Record = { + address: contractAddress as Address, + abi: parsedAbi, + functionName, + args + }; + const txHash = await services.writeContract(privateKey as Hex, contractParams, network); return { content: [{ type: "text", - text: JSON.stringify({ - address, - privateKey: "0x" + privateKey.replace(/^0x/, '') - }, null, 2) + text: JSON.stringify({ network, transactionHash: txHash, message: "Contract write transaction sent successfully" }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error deriving address from private key: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error writing to contract: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); -} \ No newline at end of file +} diff --git a/src/server/http-server.ts b/src/server/http-server.ts index 216672c..5774fc9 100644 --- a/src/server/http-server.ts +++ b/src/server/http-server.ts @@ -1,34 +1,23 @@ -import { config } from "dotenv"; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { randomUUID } from "node:crypto"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import startServer from "./server.js"; import express, { Request, Response } from "express"; -import cors from "cors"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -// Environment variables - hardcoded values -const PORT = 3001; -const HOST = '0.0.0.0'; +// Environment variables +const PORT = parseInt(process.env.MCP_PORT || "3001", 10); +const HOST = process.env.MCP_HOST || "0.0.0.0"; console.error(`Configured to listen on ${HOST}:${PORT}`); // Setup Express const app = express(); app.use(express.json()); -app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], - credentials: true, - exposedHeaders: ['Content-Type', 'Access-Control-Allow-Origin'] -})); -// Add OPTIONS handling for preflight requests -app.options('*', cors()); +// Track active transports by session ID +const transports = new Map(); -// Keep track of active connections with session IDs -const connections = new Map(); - -// Initialize the server +// Initialize the MCP server let server: McpServer | null = null; startServer().then(s => { server = s; @@ -38,165 +27,167 @@ startServer().then(s => { process.exit(1); }); -// Define routes -// @ts-ignore -app.get("/sse", (req: Request, res: Response) => { - console.error(`Received SSE connection request from ${req.ip}`); - console.error(`Query parameters: ${JSON.stringify(req.query)}`); - - // Set CORS headers explicitly - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - +// Handle all MCP requests through POST /mcp +app.post("/mcp", async (req: Request, res: Response) => { + console.error(`Received POST /mcp request from ${req.ip}`); + if (!server) { - console.error("Server not initialized yet, rejecting SSE connection"); - return res.status(503).send("Server not initialized"); + console.error("Server not initialized yet"); + res.status(503).json({ error: "Server not initialized" }); + return; } - - // Generate a unique session ID if one is not provided - // The sessionId is crucial for mapping SSE connections to message handlers - const sessionId = generateSessionId(); - console.error(`Creating SSE session with ID: ${sessionId}`); - - // Set SSE headers - res.setHeader("Content-Type", "text/event-stream"); - res.setHeader("Cache-Control", "no-cache, no-transform"); - res.setHeader("Connection", "keep-alive"); - - // Create transport - handle before writing to response - try { - console.error(`Creating SSE transport for session: ${sessionId}`); - - // Create and store the transport keyed by session ID - // Note: The path must match what the client expects (typically "/messages") - const transport = new SSEServerTransport("/messages", res); - connections.set(sessionId, transport); - - // Handle connection close - req.on("close", () => { - console.error(`SSE connection closed for session: ${sessionId}`); - connections.delete(sessionId); - }); - - // Connect transport to server - this must happen before sending any data - server.connect(transport).then(() => { - // Send an initial event with the session ID for the client to use in messages - // Only send this after the connection is established - console.error(`SSE connection established for session: ${sessionId}`); - - // Send the session ID to the client - res.write(`data: ${JSON.stringify({ type: "session_init", sessionId })}\n\n`); - }).catch((error: Error) => { - console.error(`Error connecting transport to server: ${error}`); - connections.delete(sessionId); + + // Check for existing session + const sessionId = req.headers["mcp-session-id"] as string | undefined; + let transport: StreamableHTTPServerTransport; + + if (sessionId && transports.has(sessionId)) { + // Reuse existing transport for this session + transport = transports.get(sessionId)!; + console.error(`Reusing transport for session: ${sessionId}`); + } else if (!sessionId) { + // New session - create transport with session ID generator + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (newSessionId) => { + console.error(`Session initialized: ${newSessionId}`); + transports.set(newSessionId, transport); + }, + onsessionclosed: (closedSessionId) => { + console.error(`Session closed: ${closedSessionId}`); + transports.delete(closedSessionId); + } }); + + // Connect the transport to the server + await server.connect(transport); + console.error("New transport connected to server"); + } else { + // Invalid session ID provided + console.error(`Invalid session ID: ${sessionId}`); + res.status(404).json({ error: "Session not found" }); + return; + } + + // Handle the request + try { + await transport.handleRequest(req, res, req.body); } catch (error) { - console.error(`Error creating SSE transport: ${error}`); - connections.delete(sessionId); - res.status(500).send(`Internal server error: ${error}`); + console.error(`Error handling request: ${error}`); + if (!res.headersSent) { + res.status(500).json({ error: `Internal server error: ${error}` }); + } } }); -// @ts-ignore -app.post("/messages", (req: Request, res: Response) => { - // Extract the session ID from the URL query parameters - let sessionId = req.query.sessionId?.toString(); - - // If no sessionId is provided and there's only one connection, use that - if (!sessionId && connections.size === 1) { - sessionId = Array.from(connections.keys())[0]; - console.error(`No sessionId provided, using the only active session: ${sessionId}`); - } - - console.error(`Received message for sessionId ${sessionId}`); - console.error(`Message body: ${JSON.stringify(req.body)}`); - - // Set CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - +// Handle GET requests for SSE streams (server-to-client notifications) +app.get("/mcp", async (req: Request, res: Response) => { + console.error(`Received GET /mcp request from ${req.ip}`); + if (!server) { - console.error("Server not initialized yet"); - return res.status(503).json({ error: "Server not initialized" }); + res.status(503).json({ error: "Server not initialized" }); + return; } - - if (!sessionId) { - console.error("No session ID provided and multiple connections exist"); - return res.status(400).json({ - error: "No session ID provided. Please provide a sessionId query parameter or connect to /sse first.", - activeConnections: connections.size - }); + + const sessionId = req.headers["mcp-session-id"] as string | undefined; + + if (!sessionId || !transports.has(sessionId)) { + res.status(400).json({ error: "Invalid or missing session ID" }); + return; } - - const transport = connections.get(sessionId); - if (!transport) { - console.error(`Session not found: ${sessionId}`); - return res.status(404).json({ error: "Session not found" }); + + const transport = transports.get(sessionId)!; + + try { + await transport.handleRequest(req, res); + } catch (error) { + console.error(`Error handling SSE request: ${error}`); + if (!res.headersSent) { + res.status(500).json({ error: `Internal server error: ${error}` }); + } } - - console.error(`Handling message for session: ${sessionId}`); +}); + +// Handle DELETE requests to close sessions +app.delete("/mcp", async (req: Request, res: Response) => { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + + if (!sessionId || !transports.has(sessionId)) { + res.status(404).json({ error: "Session not found" }); + return; + } + + const transport = transports.get(sessionId)!; + try { - transport.handlePostMessage(req, res).catch((error: Error) => { - console.error(`Error handling post message: ${error}`); - res.status(500).json({ error: `Internal server error: ${error.message}` }); - }); + await transport.handleRequest(req, res); } catch (error) { - console.error(`Exception handling post message: ${error}`); - res.status(500).json({ error: `Internal server error: ${error}` }); + console.error(`Error closing session: ${error}`); + if (!res.headersSent) { + res.status(500).json({ error: `Internal server error: ${error}` }); + } } }); -// Add a simple health check endpoint -app.get("/health", (req: Request, res: Response) => { - res.status(200).json({ +// Health check endpoint +app.get("/health", (_req: Request, res: Response) => { + res.status(200).json({ status: "ok", server: server ? "initialized" : "initializing", - activeConnections: connections.size, - connectedSessionIds: Array.from(connections.keys()) + activeSessions: transports.size, + sessionIds: Array.from(transports.keys()) }); }); -// Add a root endpoint for basic info -app.get("/", (req: Request, res: Response) => { +// Root endpoint for basic info +app.get("/", (_req: Request, res: Response) => { res.status(200).json({ - name: "MCP Server", - version: "1.0.0", + name: "EVM MCP Server", + version: "2.0.0", + protocol: "MCP 2025-06-18", + transport: "Streamable HTTP", endpoints: { - sse: "/sse", - messages: "/messages", + mcp: "/mcp", health: "/health" }, status: server ? "ready" : "initializing", - activeConnections: connections.size + activeSessions: transports.size }); }); -// Helper function to generate a UUID-like session ID -function generateSessionId(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); -} - // Handle process termination gracefully -process.on('SIGINT', () => { +process.on('SIGINT', async () => { console.error('Shutting down server...'); - connections.forEach((transport, sessionId) => { - console.error(`Closing connection for session: ${sessionId}`); - }); + + // Close all active transports + for (const [sessionId, transport] of transports) { + console.error(`Closing transport for session: ${sessionId}`); + await transport.close(); + } + transports.clear(); + + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.error('Received SIGTERM, shutting down...'); + + for (const [sessionId, transport] of transports) { + console.error(`Closing transport for session: ${sessionId}`); + await transport.close(); + } + transports.clear(); + process.exit(0); }); -// Start the HTTP server on a different port (3001) to avoid conflicts +// Start the HTTP server const httpServer = app.listen(PORT, HOST, () => { - console.error(`Template MCP Server running at http://${HOST}:${PORT}`); - console.error(`SSE endpoint: http://${HOST}:${PORT}/sse`); - console.error(`Messages endpoint: http://${HOST}:${PORT}/messages (sessionId optional if only one connection)`); + console.error(`EVM MCP Server running at http://${HOST}:${PORT}`); + console.error(`MCP endpoint: http://${HOST}:${PORT}/mcp`); console.error(`Health check: http://${HOST}:${PORT}/health`); + console.error(`Protocol: MCP 2025-06-18 (Streamable HTTP)`); }).on('error', (err: Error) => { console.error(`Server error: ${err}`); -}); \ No newline at end of file + process.exit(1); +}); diff --git a/src/server/server.ts b/src/server/server.ts index 81cf3a5..fe86134 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -7,22 +7,40 @@ import { getSupportedNetworks } from "../core/chains.js"; // Create and start the MCP server async function startServer() { try { - // Create a new MCP server instance - const server = new McpServer({ - name: "EVM-Server", - version: "1.0.0" - }); + // Create a new MCP server instance with capabilities + const server = new McpServer( + { + name: "evm-mcp-server", + version: "2.0.0" + }, + { + capabilities: { + tools: { + listChanged: true + }, + resources: { + subscribe: false, + listChanged: true + }, + prompts: { + listChanged: true + }, + logging: {} + } + } + ); // Register all resources, tools, and prompts registerEVMResources(server); registerEVMTools(server); registerEVMPrompts(server); - + // Log server information - console.error(`EVM MCP Server initialized`); - console.error(`Supported networks: ${getSupportedNetworks().join(", ")}`); + console.error(`EVM MCP Server v2.0.0 initialized`); + console.error(`Protocol: MCP 2025-06-18`); + console.error(`Supported networks: ${getSupportedNetworks().length} networks`); console.error("Server is ready to handle requests"); - + return server; } catch (error) { console.error("Failed to initialize server:", error); @@ -31,4 +49,4 @@ async function startServer() { } // Export the server creation function -export default startServer; \ No newline at end of file +export default startServer; diff --git a/tsconfig.json b/tsconfig.json index b0b3d32..d2dcaca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,20 @@ { "compilerOptions": { "target": "ES2022", + "lib": ["ES2022"], "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "strict": true, + "strictNullChecks": true, "skipLibCheck": true, "outDir": "dist", "sourceMap": true, "declaration": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "build"] } From 1e1d8799394e4903548dae7ead47896851235fe8 Mon Sep 17 00:00:00 2001 From: Valentin Cart Date: Wed, 19 Nov 2025 11:24:35 -0500 Subject: [PATCH 02/11] feat: replace generic EVM prompts with new task-oriented workflows for transfers, diagnostics, wallet analysis, and approvals, and add an ABI service. --- src/core/prompts.ts | 291 ++++++++---- src/core/resources.ts | 388 +--------------- src/core/services/abi.ts | 116 +++++ src/core/services/index.ts | 1 + src/core/tools.ts | 900 ++++++++++++++++++------------------- 5 files changed, 779 insertions(+), 917 deletions(-) create mode 100644 src/core/services/abi.ts diff --git a/src/core/prompts.ts b/src/core/prompts.ts index fd9dc52..eb8a01c 100644 --- a/src/core/prompts.ts +++ b/src/core/prompts.ts @@ -2,41 +2,73 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; /** - * Register all EVM-related prompts with the MCP server + * Register task-oriented prompts with the MCP server + * + * Prompts function like macros - they guide the model through complex multi-step workflows. + * Instead of the AI agent having to discover and call multiple tools in the right sequence, + * these prompts provide a structured approach to common blockchain tasks. + * * @param server The MCP server instance */ export function registerEVMPrompts(server: McpServer) { - // Basic block explorer prompt + // ============================================================================ + // TRANSACTION PREPARATION PROMPTS + // ============================================================================ + server.registerPrompt( - "explore_block", + "prepare_transfer", { - description: "Explore information about a specific block", + description: "Guide through preparing a token transfer with safety checks", argsSchema: { - blockNumber: z.string().optional().describe("Block number to explore. If not provided, latest block will be used."), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + tokenType: z.enum(["native", "erc20"]).describe("Type of token: 'native' for ETH/MATIC, 'erc20' for contract tokens"), + recipient: z.string().describe("Recipient address or ENS name"), + amount: z.string().describe("Amount to transfer"), + network: z.string().optional().describe("Network name (e.g., 'ethereum', 'polygon'). Defaults to Ethereum mainnet."), + tokenAddress: z.string().optional().describe("Token contract address (required for ERC20 transfers)") } }, - ({ blockNumber, network = "ethereum" }) => ({ + ({ tokenType, recipient, amount, network = "ethereum", tokenAddress }) => ({ messages: [{ role: "user", content: { type: "text", - text: blockNumber - ? `Please analyze block #${blockNumber} on the ${network} network and provide information about its key metrics, transactions, and significance.` - : `Please analyze the latest block on the ${network} network and provide information about its key metrics, transactions, and significance.` + text: tokenType === "native" + ? `I want to transfer ${amount} native tokens (${network === "ethereum" ? "ETH" : "MATIC"}) to ${recipient} on ${network}. + +Before proceeding: +1. Call get_wallet_address to confirm which wallet will send the transaction +2. Call get_balance to verify I have enough balance +3. Call get_gas_price to understand current gas costs +4. Summarize the transaction details and ask for confirmation before executing +5. Call transfer_native once approved +6. Wait for confirmation with wait_for_transaction + +Be clear about gas costs and the impact of this transaction.` + : `I want to transfer ${amount} tokens (contract: ${tokenAddress}) to ${recipient} on ${network}. + +Before proceeding: +1. Call get_wallet_address to confirm which wallet will send the transaction +2. Call get_token_balance to verify I have enough balance +3. Call get_gas_price to understand current gas costs +4. Check if an approval is needed (call get_allowance for your DEX/protocol) +5. Summarize the transaction details and ask for confirmation before executing +6. If approval needed, call approve_token_spending first +7. Call transfer_erc20 once approved +8. Wait for confirmation with wait_for_transaction + +Be clear about gas costs, token decimals, and the impact of this transaction.` } }] }) ); - // Transaction analysis prompt server.registerPrompt( - "analyze_transaction", + "diagnose_transaction", { - description: "Analyze a specific transaction", + description: "Analyze a failed or pending transaction and suggest solutions", argsSchema: { - txHash: z.string().describe("Transaction hash to analyze"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + txHash: z.string().describe("Transaction hash (0x...)"), + network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet.") } }, ({ txHash, network = "ethereum" }) => ({ @@ -44,64 +76,146 @@ export function registerEVMPrompts(server: McpServer) { role: "user", content: { type: "text", - text: `Please analyze transaction ${txHash} on the ${network} network and provide a detailed explanation of what this transaction does, who the parties involved are, the amount transferred (if applicable), gas used, and any other relevant information.` + text: `Please diagnose this transaction: ${txHash} on ${network} + +Follow these steps: +1. Call get_transaction to get the transaction details +2. Call get_transaction_receipt to check the status and gas used +3. Analyze what went wrong or what the transaction is doing +4. If it failed: + - Check if it was out of gas + - Check if it was a contract revert + - Look at the gas limit vs gas used +5. Provide: + - Current status (pending/confirmed/failed) + - Why it failed (if applicable) + - Gas analysis + - Recommended next steps + +Be specific about the issues and provide actionable solutions.` } }] }) ); - // Address analysis prompt + // ============================================================================ + // WALLET ANALYSIS PROMPTS + // ============================================================================ + server.registerPrompt( - "analyze_address", + "analyze_wallet", { - description: "Analyze an EVM address", + description: "Get a comprehensive overview of a wallet's assets and activity", argsSchema: { - address: z.string().describe("Ethereum address to analyze"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + address: z.string().describe("Wallet address or ENS name to analyze"), + network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet."), + tokens: z.string().optional().describe("Comma-separated list of ERC20 token addresses to check balance for") } }, - ({ address, network = "ethereum" }) => ({ + ({ address, network = "ethereum", tokens }) => { + const tokenList = tokens ? tokens.split(',').map(t => t.trim()) : []; + return { messages: [{ role: "user", content: { type: "text", - text: `Please analyze the address ${address} on the ${network} network. Provide information about its balance, transaction count, and any other relevant information you can find.` + text: `Please analyze the wallet ${address} on ${network}: + +1. Call resolve_ens_name if the input looks like an ENS name, otherwise use it as an address +2. Call get_balance to get the native token balance +3. If tokens are specified, call get_token_balance for each token to show holdings +4. Provide a summary with: + - Wallet address and ENS name (if any) + - Native token balance (in both wei and ether) + - Token holdings (if checked) + - Overall asset overview + +Format the results in a clear, readable way. If the wallet has no balance, let the user know.` + } + }] + }; + } + ); + + server.registerPrompt( + "audit_approvals", + { + description: "Review token approvals for a wallet and identify security risks", + argsSchema: { + address: z.string().optional().describe("Wallet address to audit (defaults to configured wallet)"), + tokenAddress: z.string().describe("ERC20 token contract address to check approvals for"), + network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet.") + } + }, + ({ address, tokenAddress, network = "ethereum" }) => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: `Please audit token approvals for ${tokenAddress} on ${network}${address ? ` for address ${address}` : " for the configured wallet"}: + +1. If no address is provided, call get_wallet_address to get the configured wallet +2. Call get_allowance to check if there are any existing approvals +3. Analyze the approval amount and what it means: + - If allowance is 0: No approval set + - If allowance is a normal amount: Limited approval (safe) + - If allowance is max uint256: Unlimited approval (security risk) +4. Provide recommendations: + - Is the current approval appropriate? + - Should any dangerous approvals be revoked? + - What approvals should be set up before interacting with protocols? + +Be clear about the security implications of unlimited approvals.` } }] }) ); - // Smart contract interaction guidance + // ============================================================================ + // SMART CONTRACT EXPLORATION PROMPTS + // ============================================================================ + server.registerPrompt( - "interact_with_contract", + "explore_contract", { - description: "Get guidance on interacting with a smart contract", + description: "Analyze a smart contract to understand its functions and state", argsSchema: { - contractAddress: z.string().describe("The contract address"), - abiJson: z.string().optional().describe("The contract ABI as a JSON string"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + contractAddress: z.string().describe("The contract address to explore"), + network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet.") } }, - ({ contractAddress, abiJson, network = "ethereum" }) => ({ + ({ contractAddress, network = "ethereum" }) => ({ messages: [{ role: "user", content: { type: "text", - text: abiJson - ? `I need to interact with the smart contract at address ${contractAddress} on the ${network} network. Here's the ABI:\n\n${abiJson}\n\nPlease analyze this contract's functions and provide guidance on how to interact with it safely.` - : `I need to interact with the smart contract at address ${contractAddress} on the ${network} network. Please help me understand what this contract does and how I can interact with it safely.` + text: `Please explore the smart contract at ${contractAddress} on ${network}: + +1. This is a read-only exploration - no transactions will be executed +2. Identify what type of contract this is (token, NFT, DEX, lending, etc.) +3. Try to read common functions that might exist: + - For tokens: name(), symbol(), decimals(), totalSupply() + - For NFTs: name(), symbol(), totalSupply() + - For other contracts: relevant state functions +4. Use read_contract to call these functions +5. Provide a summary with: + - Contract type and purpose (best guess) + - Key properties discovered + - Security notes (if applicable) + - Next steps for interacting with it + +Be clear about what you were able to discover and what remains unknown.` } }] }) ); - // EVM concept explanation server.registerPrompt( "explain_evm_concept", { - description: "Get an explanation of an EVM concept", + description: "Get an explanation of an EVM or blockchain concept", argsSchema: { - concept: z.string().describe("The EVM concept to explain (e.g., gas, nonce, etc.)") + concept: z.string().describe("The concept to explain (e.g., 'gas', 'nonce', 'smart contracts', 'MEV')") } }, ({ concept }) => ({ @@ -109,67 +223,92 @@ export function registerEVMPrompts(server: McpServer) { role: "user", content: { type: "text", - text: `Please explain the EVM Blockchain concept of "${concept}" in detail. Include how it works, why it's important, and provide examples if applicable.` + text: `Please explain the blockchain/EVM concept: "${concept}" + +In your explanation: +1. Define what it is in simple terms +2. Explain why it matters +3. Give practical examples +4. Explain how it affects using blockchain applications +5. If relevant, mention how to check or monitor it on the blockchain + +Make it accessible for someone new to blockchain but interested in using EVM networks.` } }] }) ); - // Network comparison + // ============================================================================ + // NETWORK INFORMATION PROMPTS + // ============================================================================ + server.registerPrompt( "compare_networks", { - description: "Compare different EVM-compatible networks", + description: "Compare different EVM networks to understand their differences", argsSchema: { - networkList: z.string().describe("Comma-separated list of networks to compare") + networks: z.string().describe("Comma-separated list of network names to compare (e.g., 'ethereum,polygon,arbitrum')") } }, - ({ networkList }) => { - const networks = networkList.split(',').map(n => n.trim()); + ({ networks }) => { + const networkList = networks.split(',').map(n => n.trim()); return { - messages: [{ - role: "user", - content: { - type: "text", - text: `Please compare the following EVM-compatible networks: ${networks.join(', ')}. Include information about their architecture, gas fees, transaction speed, security, and any other relevant differences.` - } - }] + messages: [{ + role: "user", + content: { + type: "text", + text: `Please compare these EVM networks: ${networkList.join(", ")} + +For each network: +1. Call get_chain_info to get current chain ID and block number +2. Call get_gas_price to understand current gas costs +3. Provide comparison across: + - Chain characteristics (mainnet, testnet, etc.) + - Current gas prices + - Block time and finality + - Typical use cases and advantages + - Any known limitations or risks +4. Provide a recommendation for which network to use based on: + - Transaction speed needs + - Cost considerations + - Ecosystem size and liquidity + - Security considerations + +Make the comparison easy to understand for someone deciding which network to use.` + } + }] }; } ); - // Token analysis prompt server.registerPrompt( - "analyze_token", + "check_network_status", { - description: "Analyze an ERC20 or NFT token", + description: "Check the current status and health of an EVM network", argsSchema: { - tokenAddress: z.string().describe("Token contract address to analyze"), - tokenType: z.string().optional().describe("Type of token (erc20, erc721/nft, or auto-detect)"), - tokenId: z.string().optional().describe("Token ID (required for NFT analysis)"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet.") } }, - ({ tokenAddress, tokenType = "auto", tokenId, network = "ethereum" }) => { - let promptText = ""; - - if (tokenType === "erc20" || tokenType === "auto") { - promptText = `Please analyze the ERC20 token at address ${tokenAddress} on the ${network} network. Provide information about its name, symbol, total supply, and any other relevant details.`; - } else if ((tokenType === "erc721" || tokenType === "nft") && tokenId) { - promptText = `Please analyze the NFT with token ID ${tokenId} from the collection at address ${tokenAddress} on the ${network} network. Provide information about the collection name, token details, and any other relevant information.`; - } else if (tokenType === "nft" || tokenType === "erc721") { - promptText = `Please analyze the NFT collection at address ${tokenAddress} on the ${network} network. Provide information about the collection name, symbol, total supply if available, and any other relevant details.`; - } + ({ network = "ethereum" }) => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: `Please check the current status of the ${network} network: - return { - messages: [{ - role: "user", - content: { - type: "text", - text: promptText - } - }] - }; - } +1. Call get_chain_info to get chain ID, current block number, and RPC info +2. Call get_latest_block to see recent block information +3. Call get_gas_price to check current gas prices +4. Provide a status report with: + - Network is healthy/operational + - Current block number and recent block times + - Current gas prices (base fee and priority fee) + - Any observations about network congestion + - Recommendations for transaction timing if applicable + +Be clear about whether now is a good time to transact on this network.` + } + }] + }) ); } diff --git a/src/core/resources.ts b/src/core/resources.ts index 7ef0879..0e97523 100644 --- a/src/core/resources.ts +++ b/src/core/resources.ts @@ -1,399 +1,39 @@ -import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { getSupportedNetworks, getRpcUrl } from "./chains.js"; -import * as services from "./services/index.js"; -import type { Address, Hash } from "viem"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { getSupportedNetworks } from "./chains.js"; /** - * Register all EVM-related resources + * Register EVM-related resources with the MCP server + * + * Resources are application-driven, read-only data that clients can explicitly load. + * For an AI agent use case, most data should be exposed through tools instead, + * which allow the model to discover and autonomously fetch information. + * + * The supported_networks resource provides a static reference list that clients + * may want to browse when configuring which networks to use. + * * @param server The MCP server instance */ export function registerEVMResources(server: McpServer) { - // ============================================================================ - // CHAIN INFO RESOURCES - // ============================================================================ - - server.registerResource( - "chain_info_by_network", - new ResourceTemplate("evm://{network}/chain", { list: undefined }), - { description: "Get chain information for a specific network", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const chainId = await services.getChainId(network); - const blockNumber = await services.getBlockNumber(network); - const rpcUrl = getRpcUrl(network); - return { contents: [{ uri: uri.href, text: JSON.stringify({ network, chainId, blockNumber: blockNumber.toString(), rpcUrl }, null, 2) }] }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - server.registerResource( - "ethereum_chain_info", - "evm://chain", - { description: "Get Ethereum mainnet chain information", mimeType: "application/json" }, - async (uri) => { - try { - const network = "ethereum"; - const chainId = await services.getChainId(network); - const blockNumber = await services.getBlockNumber(network); - const rpcUrl = getRpcUrl(network); - return { contents: [{ uri: uri.href, text: JSON.stringify({ network, chainId, blockNumber: blockNumber.toString(), rpcUrl }, null, 2) }] }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - server.registerResource( "supported_networks", "evm://networks", - { description: "Get list of all supported EVM networks", mimeType: "application/json" }, + { description: "Get list of all supported EVM networks and their configuration", mimeType: "application/json" }, async (uri) => { try { const networks = getSupportedNetworks(); - return { contents: [{ uri: uri.href, text: JSON.stringify({ supportedNetworks: networks }, null, 2) }] }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ============================================================================ - // BLOCK RESOURCES - // ============================================================================ - - server.registerResource( - "evm_block_by_number", - new ResourceTemplate("evm://{network}/block/{blockNumber}", { list: undefined }), - { description: "Get block by number for a specific network", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const blockNumber = params.blockNumber as string; - const block = await services.getBlockByNumber(parseInt(blockNumber), network); - return { contents: [{ uri: uri.href, text: services.helpers.formatJson(block) }] }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - server.registerResource( - "block_by_hash", - new ResourceTemplate("evm://{network}/block/hash/{blockHash}", { list: undefined }), - { description: "Get block by hash for a specific network", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const blockHash = params.blockHash as string; - const block = await services.getBlockByHash(blockHash as Hash, network); - return { contents: [{ uri: uri.href, text: services.helpers.formatJson(block) }] }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - server.registerResource( - "evm_latest_block", - new ResourceTemplate("evm://{network}/block/latest", { list: undefined }), - { description: "Get latest block for a specific network", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const block = await services.getLatestBlock(network); - return { contents: [{ uri: uri.href, text: services.helpers.formatJson(block) }] }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - server.registerResource( - "default_latest_block", - "evm://block/latest", - { description: "Get latest Ethereum mainnet block", mimeType: "application/json" }, - async (uri) => { - try { - const block = await services.getLatestBlock("ethereum"); - return { contents: [{ uri: uri.href, text: services.helpers.formatJson(block) }] }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ============================================================================ - // BALANCE RESOURCES - // ============================================================================ - - server.registerResource( - "evm_address_native_balance", - new ResourceTemplate("evm://{network}/address/{address}/balance", { list: undefined }), - { description: "Get native token balance for an address on a specific network", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const address = params.address as string; - const balance = await services.getETHBalance(address as Address, network); - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ network, address, balance: { wei: balance.wei.toString(), ether: balance.ether } }, null, 2) - }] - }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - server.registerResource( - "default_eth_balance", - new ResourceTemplate("evm://address/{address}/eth-balance", { list: undefined }), - { description: "Get ETH balance on Ethereum mainnet", mimeType: "application/json" }, - async (uri, params) => { - try { - const address = params.address as string; - const balance = await services.getETHBalance(address as Address, "ethereum"); - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ network: "ethereum", address, balance: { wei: balance.wei.toString(), ether: balance.ether } }, null, 2) - }] - }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - server.registerResource( - "erc20_balance", - new ResourceTemplate("evm://{network}/address/{address}/token/{tokenAddress}/balance", { list: undefined }), - { description: "Get ERC20 token balance for an address", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const address = params.address as string; - const tokenAddress = params.tokenAddress as string; - const balance = await services.getERC20Balance(tokenAddress as Address, address as Address, network); - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - network, address, tokenAddress, - balance: { raw: balance.raw.toString(), formatted: balance.formatted, decimals: balance.token.decimals } - }, null, 2) - }] - }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - server.registerResource( - "default_erc20_balance", - new ResourceTemplate("evm://address/{address}/token/{tokenAddress}/balance", { list: undefined }), - { description: "Get ERC20 token balance on Ethereum mainnet", mimeType: "application/json" }, - async (uri, params) => { - try { - const address = params.address as string; - const tokenAddress = params.tokenAddress as string; - const balance = await services.getERC20Balance(tokenAddress as Address, address as Address, "ethereum"); return { contents: [{ uri: uri.href, - text: JSON.stringify({ - network: "ethereum", address, tokenAddress, - balance: { raw: balance.raw.toString(), formatted: balance.formatted, decimals: balance.token.decimals } - }, null, 2) + text: JSON.stringify({ supportedNetworks: networks }, null, 2) }] }; } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ============================================================================ - // TRANSACTION RESOURCES - // ============================================================================ - - server.registerResource( - "evm_transaction_details", - new ResourceTemplate("evm://{network}/tx/{txHash}", { list: undefined }), - { description: "Get transaction details by hash", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const txHash = params.txHash as string; - const tx = await services.getTransaction(txHash as Hash, network); - return { contents: [{ uri: uri.href, text: services.helpers.formatJson(tx) }] }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - server.registerResource( - "default_transaction_by_hash", - new ResourceTemplate("evm://tx/{txHash}", { list: undefined }), - { description: "Get Ethereum mainnet transaction by hash", mimeType: "application/json" }, - async (uri, params) => { - try { - const txHash = params.txHash as string; - const tx = await services.getTransaction(txHash as Hash, "ethereum"); - return { contents: [{ uri: uri.href, text: services.helpers.formatJson(tx) }] }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ============================================================================ - // TOKEN INFO RESOURCES - // ============================================================================ - - server.registerResource( - "erc20_token_details", - new ResourceTemplate("evm://{network}/token/{tokenAddress}", { list: undefined }), - { description: "Get ERC20 token details", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - const tokenInfo = await services.getERC20TokenInfo(tokenAddress, network); - return { contents: [{ uri: uri.href, text: JSON.stringify({ address: tokenAddress, network, ...tokenInfo }, null, 2) }] }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - server.registerResource( - "erc20_token_address_balance", - new ResourceTemplate("evm://{network}/token/{tokenAddress}/balanceOf/{address}", { list: undefined }), - { description: "Get ERC20 token balance for an address", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - const address = params.address as Address; - const balance = await services.getERC20Balance(tokenAddress, address, network); return { contents: [{ uri: uri.href, - text: JSON.stringify({ - tokenAddress, owner: address, network, - raw: balance.raw.toString(), formatted: balance.formatted, - symbol: balance.token.symbol, decimals: balance.token.decimals - }, null, 2) + text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ============================================================================ - // NFT (ERC721) RESOURCES - // ============================================================================ - - server.registerResource( - "erc721_nft_token_details", - new ResourceTemplate("evm://{network}/nft/{tokenAddress}/{tokenId}", { list: undefined }), - { description: "Get NFT token details", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - const tokenId = BigInt(params.tokenId as string); - const nftInfo = await services.getERC721TokenMetadata(tokenAddress, tokenId, network); - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ contract: tokenAddress, tokenId: tokenId.toString(), network, ...nftInfo }, null, 2) - }] - }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - server.registerResource( - "erc721_nft_ownership_check", - new ResourceTemplate("evm://{network}/nft/{tokenAddress}/{tokenId}/isOwnedBy/{address}", { list: undefined }), - { description: "Check NFT ownership", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - const tokenId = BigInt(params.tokenId as string); - const address = params.address as Address; - const isOwner = await services.isNFTOwner(tokenAddress, address, tokenId, network); - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ contract: tokenAddress, tokenId: tokenId.toString(), owner: address, network, isOwner }, null, 2) - }] - }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ============================================================================ - // ERC1155 RESOURCES - // ============================================================================ - - server.registerResource( - "erc1155_token_metadata_uri", - new ResourceTemplate("evm://{network}/erc1155/{tokenAddress}/{tokenId}/uri", { list: undefined }), - { description: "Get ERC1155 token metadata URI", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - const tokenId = BigInt(params.tokenId as string); - const tokenURI = await services.getERC1155TokenURI(tokenAddress, tokenId, network); - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ contract: tokenAddress, tokenId: tokenId.toString(), network, uri: tokenURI }, null, 2) - }] - }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - server.registerResource( - "erc1155_token_address_balance", - new ResourceTemplate("evm://{network}/erc1155/{tokenAddress}/{tokenId}/balanceOf/{address}", { list: undefined }), - { description: "Get ERC1155 token balance", mimeType: "application/json" }, - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - const tokenId = BigInt(params.tokenId as string); - const address = params.address as Address; - const balance = await services.getERC1155Balance(tokenAddress, address, tokenId, network); - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ contract: tokenAddress, tokenId: tokenId.toString(), owner: address, network, balance: balance.toString() }, null, 2) - }] - }; - } catch (error) { - return { contents: [{ uri: uri.href, text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); diff --git a/src/core/services/abi.ts b/src/core/services/abi.ts new file mode 100644 index 0000000..d61368a --- /dev/null +++ b/src/core/services/abi.ts @@ -0,0 +1,116 @@ +import { type Address } from 'viem'; + +/** + * Fetch contract ABI from Etherscan API + * Requires ETHERSCAN_API_KEY environment variable to be set + * + * @param contractAddress The contract address to fetch ABI for + * @param network The network to use (etherscan, polygonscan, etc.) + * @returns The contract ABI as a JSON string + */ +export async function fetchContractABI( + contractAddress: Address, + network: string = 'ethereum' +): Promise { + const apiKey = process.env.ETHERSCAN_API_KEY; + if (!apiKey) { + throw new Error('ETHERSCAN_API_KEY environment variable is not set. Set it to fetch contract ABIs.'); + } + + // Map network names to Etherscan-compatible API domains + const networkMap: { [key: string]: string } = { + 'ethereum': 'https://api.etherscan.io', + 'sepolia': 'https://api-sepolia.etherscan.io', + 'polygon': 'https://api.polygonscan.com', + 'mumbai': 'https://api-testnet.polygonscan.com', + 'arbitrum': 'https://api.arbiscan.io', + 'arbitrum-sepolia': 'https://api-sepolia.arbiscan.io', + 'optimism': 'https://api-optimistic.etherscan.io', + 'optimism-sepolia': 'https://api-sepolia-optimistic.etherscan.io', + 'base': 'https://api.basescan.org', + 'base-sepolia': 'https://api-sepolia.basescan.org', + 'avalanche': 'https://api.snowtrace.io', + 'avalanche-fuji': 'https://api-testnet.snowtrace.io', + }; + + const apiUrl = networkMap[network.toLowerCase()]; + if (!apiUrl) { + throw new Error(`Network "${network}" is not supported for ABI fetching. Supported: ${Object.keys(networkMap).join(', ')}`); + } + + try { + const url = new URL(`${apiUrl}/api`); + url.searchParams.set('module', 'contract'); + url.searchParams.set('action', 'getabi'); + url.searchParams.set('address', contractAddress); + url.searchParams.set('apikey', apiKey); + + const response = await fetch(url.toString()); + const data = await response.json() as any; + + if (data.status === '0') { + throw new Error(data.result || 'Failed to fetch ABI from block explorer'); + } + + if (!data.result) { + throw new Error('No ABI found for this contract. Contract might not be verified.'); + } + + return data.result; + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to fetch ABI: ${error.message}`); + } + throw error; + } +} + +/** + * Parse and validate an ABI JSON string + * @param abiJson The ABI as a JSON string + * @returns Parsed ABI array + */ +export function parseABI(abiJson: string): any[] { + try { + const abi = JSON.parse(abiJson); + if (!Array.isArray(abi)) { + throw new Error('ABI must be a JSON array'); + } + return abi; + } catch (error) { + throw new Error(`Invalid ABI JSON: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Get list of readable functions from an ABI + * @param abi The contract ABI + * @returns Array of read-only function names + */ +export function getReadableFunctions(abi: any[]): string[] { + return abi + .filter(item => + item.type === 'function' && + (item.stateMutability === 'view' || item.stateMutability === 'pure') + ) + .map(item => item.name) + .filter(Boolean); +} + +/** + * Get a specific function from an ABI + * @param abi The contract ABI + * @param functionName The function name to find + * @returns The function ABI object + */ +export function getFunctionFromABI(abi: any[], functionName: string): any { + const fn = abi.find(item => + item.type === 'function' && item.name === functionName + ); + + if (!fn) { + throw new Error(`Function "${functionName}" not found in ABI`); + } + + return fn; +} diff --git a/src/core/services/index.ts b/src/core/services/index.ts index f7da50a..27345b7 100644 --- a/src/core/services/index.ts +++ b/src/core/services/index.ts @@ -7,6 +7,7 @@ export * from './transactions.js'; export * from './contracts.js'; export * from './tokens.js'; export * from './ens.js'; +export * from './abi.js'; export { utils as helpers } from './utils.js'; // Re-export common types for convenience diff --git a/src/core/tools.ts b/src/core/tools.ts index 1b2c213..24c1eeb 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -4,16 +4,68 @@ import { getSupportedNetworks, getRpcUrl } from "./chains.js"; import * as services from "./services/index.js"; import { type Address, type Hex, type Hash } from 'viem'; import { normalize } from 'viem/ens'; +import { privateKeyToAccount } from 'viem/accounts'; /** * Register all EVM-related tools with the MCP server * - * All tools that accept Ethereum addresses also support ENS names (e.g., 'vitalik.eth'). + * SECURITY: The EVM_PRIVATE_KEY environment variable must be set for write operations. + * Private keys are never passed as tool arguments for security reasons. + * Tools will use the configured wallet for all transactions. + * + * All tools that accept addresses also support ENS names (e.g., 'vitalik.eth'). * ENS names are automatically resolved to addresses using the Ethereum Name Service. * * @param server The MCP server instance */ export function registerEVMTools(server: McpServer) { + // Helper to get the configured wallet from environment + const getConfiguredWallet = () => { + const privateKey = process.env.EVM_PRIVATE_KEY; + if (!privateKey) { + throw new Error("EVM_PRIVATE_KEY environment variable is not set. Configure it to enable write operations."); + } + return privateKeyToAccount(privateKey as Hex); + }; + + // ============================================================================ + // WALLET INFORMATION TOOLS (Read-only) + // ============================================================================ + + server.registerTool( + "get_wallet_address", + { + description: "Get the address of the configured wallet. Use this to verify which wallet is active.", + inputSchema: {}, + annotations: { + title: "Get Wallet Address", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } + }, + async () => { + try { + const wallet = getConfiguredWallet(); + return { + content: [{ + type: "text", + text: JSON.stringify({ + address: wallet.address, + message: "This is the wallet that will be used for all transactions" + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], + isError: true + }; + } + } + ); + // ============================================================================ // NETWORK INFORMATION TOOLS (Read-only) // ============================================================================ @@ -21,9 +73,9 @@ export function registerEVMTools(server: McpServer) { server.registerTool( "get_chain_info", { - description: "Get information about an EVM network including chain ID, block number, and RPC URL", + description: "Get information about an EVM network: chain ID, current block number, and RPC endpoint", inputSchema: { - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base') or chain ID. Defaults to Ethereum mainnet.") }, annotations: { title: "Get Chain Info", @@ -75,7 +127,7 @@ export function registerEVMTools(server: McpServer) { }; } catch (error) { return { - content: [{ type: "text", text: `Error fetching supported networks: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -83,38 +135,42 @@ export function registerEVMTools(server: McpServer) { ); server.registerTool( - "resolve_ens", + "get_gas_price", { - description: "Resolve an ENS name to an Ethereum address", + description: "Get current gas prices (base fee, standard, and fast) for a network", inputSchema: { - ensName: z.string().describe("ENS name to resolve (e.g., 'vitalik.eth')"), - network: z.string().optional().describe("Network name or chain ID. ENS resolution works best on Ethereum mainnet. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Resolve ENS Name", + title: "Get Gas Prices", readOnlyHint: true, destructiveHint: false, - idempotentHint: true, + idempotentHint: false, openWorldHint: true } }, - async ({ ensName, network = "ethereum" }) => { + async ({ network = "ethereum" }) => { try { - if (!ensName.includes('.')) { - return { - content: [{ type: "text", text: `Error: Input "${ensName}" is not a valid ENS name. ENS names must contain a dot (e.g., 'name.eth').` }], - isError: true - }; - } - const normalizedEns = normalize(ensName); - const address = await services.resolveAddress(ensName, network); + const client = await services.getPublicClient(network); + const [baseFee, priorityFee] = await Promise.all([ + client.getGasPrice(), + client.estimateMaxPriorityFeePerGas() + ]); return { - content: [{ type: "text", text: JSON.stringify({ ensName, normalizedName: normalizedEns, resolvedAddress: address, network }, null, 2) }] + content: [{ + type: "text", + text: JSON.stringify({ + network, + baseFeePerGas: baseFee.toString(), + priorityFeePerGas: priorityFee?.toString() || "N/A", + currency: "wei" + }, null, 2) + }] }; } catch (error) { return { - content: [{ type: "text", text: `Error resolving ENS name: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error fetching gas prices: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -122,32 +178,50 @@ export function registerEVMTools(server: McpServer) { ); // ============================================================================ - // BLOCK TOOLS (Read-only) + // ENS TOOLS (Read-only) // ============================================================================ server.registerTool( - "get_block_by_number", + "resolve_ens_name", { - description: "Get a block by its block number", + description: "Resolve an ENS name to an Ethereum address", inputSchema: { - blockNumber: z.number().describe("The block number to fetch"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + ensName: z.string().describe("ENS name to resolve (e.g., 'vitalik.eth')"), + network: z.string().optional().describe("Network name or chain ID. ENS resolution works best on Ethereum mainnet. Defaults to Ethereum mainnet.") }, annotations: { - title: "Get Block by Number", + title: "Resolve ENS Name", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, - async ({ blockNumber, network = "ethereum" }) => { + async ({ ensName, network = "ethereum" }) => { try { - const block = await services.getBlockByNumber(blockNumber, network); - return { content: [{ type: "text", text: services.helpers.formatJson(block) }] }; + if (!ensName.includes('.')) { + return { + content: [{ type: "text", text: `Error: "${ensName}" is not a valid ENS name. ENS names must contain a dot (e.g., 'name.eth').` }], + isError: true + }; + } + const normalizedEns = normalize(ensName); + const address = await services.resolveAddress(ensName, network); + + return { + content: [{ + type: "text", + text: JSON.stringify({ + ensName, + normalizedName: normalizedEns, + resolvedAddress: address, + network + }, null, 2) + }] + }; } catch (error) { return { - content: [{ type: "text", text: `Error fetching block ${blockNumber}: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error resolving ENS name: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -155,27 +229,40 @@ export function registerEVMTools(server: McpServer) { ); server.registerTool( - "get_latest_block", + "lookup_ens_address", { - description: "Get the latest block from the EVM network", + description: "Lookup the ENS name for an Ethereum address (reverse resolution)", inputSchema: { + address: z.string().describe("Ethereum address to lookup"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Get Latest Block", + title: "Lookup ENS Address", readOnlyHint: true, destructiveHint: false, - idempotentHint: false, + idempotentHint: true, openWorldHint: true } }, - async ({ network = "ethereum" }) => { + async ({ address, network = "ethereum" }) => { try { - const block = await services.getLatestBlock(network); - return { content: [{ type: "text", text: services.helpers.formatJson(block) }] }; + const client = await services.getPublicClient(network); + const ensName = await client.getEnsName({ + address: address as Address + }); + return { + content: [{ + type: "text", + text: JSON.stringify({ + address, + ensName: ensName || "No ENS name found", + network + }, null, 2) + }] + }; } catch (error) { return { - content: [{ type: "text", text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error looking up ENS name: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -183,34 +270,39 @@ export function registerEVMTools(server: McpServer) { ); // ============================================================================ - // BALANCE TOOLS (Read-only) + // BLOCK TOOLS (Read-only) // ============================================================================ server.registerTool( - "get_balance", + "get_block", { - description: "Get the native token balance (ETH, MATIC, etc.) for an address", + description: "Get block details by block number or hash", inputSchema: { - address: z.string().describe("The wallet address or ENS name to check the balance for"), + blockIdentifier: z.string().describe("Block number (as string) or block hash"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Get Native Balance", + title: "Get Block", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, - async ({ address, network = "ethereum" }) => { + async ({ blockIdentifier, network = "ethereum" }) => { try { - const balance = await services.getETHBalance(address, network); - return { - content: [{ type: "text", text: JSON.stringify({ address, network, wei: balance.wei.toString(), ether: balance.ether }, null, 2) }] - }; + let block; + if (blockIdentifier.startsWith("0x") && blockIdentifier.length === 66) { + // It's a hash + block = await services.getBlockByHash(blockIdentifier as Hash, network); + } else { + // It's a number + block = await services.getBlockByNumber(parseInt(blockIdentifier), network); + } + return { content: [{ type: "text", text: services.helpers.formatJson(block) }] }; } catch (error) { return { - content: [{ type: "text", text: `Error fetching balance: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error fetching block: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -218,76 +310,69 @@ export function registerEVMTools(server: McpServer) { ); server.registerTool( - "get_erc20_balance", + "get_latest_block", { - description: "Get the ERC20 token balance of an address", + description: "Get the latest block from the network", inputSchema: { - address: z.string().describe("The address to check"), - tokenAddress: z.string().describe("The ERC20 token contract address"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Get ERC20 Balance", + title: "Get Latest Block", readOnlyHint: true, destructiveHint: false, - idempotentHint: true, + idempotentHint: false, openWorldHint: true } }, - async ({ address, tokenAddress, network = "ethereum" }) => { + async ({ network = "ethereum" }) => { try { - const balance = await services.getERC20Balance(tokenAddress as Address, address as Address, network); - return { - content: [{ - type: "text", - text: JSON.stringify({ - address, tokenAddress, network, - balance: { raw: balance.raw.toString(), formatted: balance.formatted, decimals: balance.token.decimals } - }, null, 2) - }] - }; + const block = await services.getLatestBlock(network); + return { content: [{ type: "text", text: services.helpers.formatJson(block) }] }; } catch (error) { return { - content: [{ type: "text", text: `Error fetching ERC20 balance: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); + // ============================================================================ + // BALANCE TOOLS (Read-only) + // ============================================================================ + server.registerTool( - "get_token_balance", + "get_balance", { - description: "Get the balance of an ERC20 token for an address", + description: "Get the native token balance (ETH, MATIC, etc.) for an address", inputSchema: { - tokenAddress: z.string().describe("The contract address of the ERC20 token"), - ownerAddress: z.string().describe("The wallet address or ENS name to check the balance for"), + address: z.string().describe("The wallet address or ENS name"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Get Token Balance", + title: "Get Native Token Balance", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, - async ({ tokenAddress, ownerAddress, network = "ethereum" }) => { + async ({ address, network = "ethereum" }) => { try { - const balance = await services.getERC20Balance(tokenAddress, ownerAddress, network); + const balance = await services.getETHBalance(address as Address, network); return { content: [{ type: "text", text: JSON.stringify({ - tokenAddress, owner: ownerAddress, network, - raw: balance.raw.toString(), formatted: balance.formatted, - symbol: balance.token.symbol, decimals: balance.token.decimals + network, + address, + balance: { wei: balance.wei.toString(), ether: balance.ether } }, null, 2) }] }; } catch (error) { return { - content: [{ type: "text", text: `Error fetching token balance: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error fetching balance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -295,31 +380,44 @@ export function registerEVMTools(server: McpServer) { ); server.registerTool( - "get_nft_balance", + "get_token_balance", { - description: "Get the total number of NFTs owned by an address from a specific ERC721 collection", + description: "Get the ERC20 token balance for an address", inputSchema: { - tokenAddress: z.string().describe("The contract address of the NFT collection"), - ownerAddress: z.string().describe("The wallet address to check the NFT balance for"), + address: z.string().describe("The wallet address or ENS name"), + tokenAddress: z.string().describe("The ERC20 token contract address"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Get NFT Balance", + title: "Get ERC20 Token Balance", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, - async ({ tokenAddress, ownerAddress, network = "ethereum" }) => { + async ({ address, tokenAddress, network = "ethereum" }) => { try { - const balance = await services.getERC721Balance(tokenAddress as Address, ownerAddress as Address, network); + const balance = await services.getERC20Balance(tokenAddress as Address, address as Address, network); return { - content: [{ type: "text", text: JSON.stringify({ collection: tokenAddress, owner: ownerAddress, network, balance: balance.toString() }, null, 2) }] + content: [{ + type: "text", + text: JSON.stringify({ + network, + tokenAddress, + address, + balance: { + raw: balance.raw.toString(), + formatted: balance.formatted, + symbol: balance.token.symbol, + decimals: balance.token.decimals + } + }, null, 2) + }] }; } catch (error) { return { - content: [{ type: "text", text: `Error fetching NFT balance: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error fetching token balance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -327,32 +425,62 @@ export function registerEVMTools(server: McpServer) { ); server.registerTool( - "get_erc1155_balance", + "get_allowance", { - description: "Get the balance of a specific ERC1155 token ID owned by an address", + description: "Check the allowance granted to a spender for a token. This tells you how much of a token an address can spend on your behalf.", inputSchema: { - tokenAddress: z.string().describe("The contract address of the ERC1155 token collection"), - tokenId: z.string().describe("The ID of the specific token to check"), - ownerAddress: z.string().describe("The wallet address to check the token balance for"), + tokenAddress: z.string().describe("The ERC20 token contract address"), + spenderAddress: z.string().describe("The address allowed to spend the token (usually a contract address)"), + ownerAddress: z.string().optional().describe("The owner address (defaults to the configured wallet)"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Get ERC1155 Balance", + title: "Get Token Allowance", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, - async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { + async ({ tokenAddress, spenderAddress, ownerAddress, network = "ethereum" }) => { try { - const balance = await services.getERC1155Balance(tokenAddress as Address, ownerAddress as Address, BigInt(tokenId), network); + const owner = ownerAddress ? (ownerAddress as Address) : getConfiguredWallet().address; + const client = await services.getPublicClient(network); + + const allowance = await client.readContract({ + address: tokenAddress as Address, + abi: [ + { + name: 'allowance', + type: 'function', + inputs: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' } + ], + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view' + } + ], + functionName: 'allowance', + args: [owner, spenderAddress as Address] + }); + return { - content: [{ type: "text", text: JSON.stringify({ contract: tokenAddress, tokenId, owner: ownerAddress, network, balance: balance.toString() }, null, 2) }] + content: [{ + type: "text", + text: JSON.stringify({ + network, + tokenAddress, + owner, + spenderAddress, + allowance: allowance.toString(), + message: allowance === 0n ? "No allowance set" : "Allowance is set" + }, null, 2) + }] }; } catch (error) { return { - content: [{ type: "text", text: `Error fetching ERC1155 balance: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error fetching allowance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -366,9 +494,9 @@ export function registerEVMTools(server: McpServer) { server.registerTool( "get_transaction", { - description: "Get detailed information about a specific transaction by its hash", + description: "Get transaction details by transaction hash", inputSchema: { - txHash: z.string().describe("The transaction hash to look up"), + txHash: z.string().describe("Transaction hash (0x...)"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { @@ -395,9 +523,9 @@ export function registerEVMTools(server: McpServer) { server.registerTool( "get_transaction_receipt", { - description: "Get a transaction receipt by its hash", + description: "Get transaction receipt (confirmation status, gas used, logs). Use this to check if a transaction has been confirmed.", inputSchema: { - txHash: z.string().describe("The transaction hash to look up"), + txHash: z.string().describe("Transaction hash (0x...)"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { @@ -410,11 +538,14 @@ export function registerEVMTools(server: McpServer) { }, async ({ txHash, network = "ethereum" }) => { try { - const receipt = await services.getTransactionReceipt(txHash as Hash, network); + const client = await services.getPublicClient(network); + const receipt = await client.getTransactionReceipt({ + hash: txHash as Hash + }); return { content: [{ type: "text", text: services.helpers.formatJson(receipt) }] }; } catch (error) { return { - content: [{ type: "text", text: `Error fetching receipt: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error fetching transaction receipt: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -422,36 +553,46 @@ export function registerEVMTools(server: McpServer) { ); server.registerTool( - "estimate_gas", + "wait_for_transaction", { - description: "Estimate the gas cost for a transaction", + description: "Wait for a transaction to be confirmed (mined). Polls the network until confirmation.", inputSchema: { - to: z.string().describe("The recipient address"), - value: z.string().optional().describe("The amount of ETH to send in ether (e.g., '0.1')"), - data: z.string().optional().describe("The transaction data as a hex string"), + txHash: z.string().describe("Transaction hash (0x...)"), + confirmations: z.number().optional().describe("Number of block confirmations required. Defaults to 1."), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Estimate Gas", + title: "Wait For Transaction", readOnlyHint: true, destructiveHint: false, - idempotentHint: true, + idempotentHint: false, openWorldHint: true } }, - async ({ to, value, data, network = "ethereum" }) => { + async ({ txHash, confirmations = 1, network = "ethereum" }) => { try { - const params: any = { to: to as Address }; - if (value) params.value = services.helpers.parseEther(value); - if (data) params.data = data as `0x${string}`; + const client = await services.getPublicClient(network); + const receipt = await client.waitForTransactionReceipt({ + hash: txHash as Hash, + confirmations + }); - const gas = await services.estimateGas(params, network); return { - content: [{ type: "text", text: JSON.stringify({ network, estimatedGas: gas.toString() }, null, 2) }] + content: [{ + type: "text", + text: JSON.stringify({ + network, + txHash, + status: receipt.status === 'success' ? 'confirmed' : 'failed', + blockNumber: receipt.blockNumber.toString(), + gasUsed: receipt.gasUsed.toString(), + confirmations + }, null, 2) + }] }; } catch (error) { return { - content: [{ type: "text", text: `Error estimating gas: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error waiting for transaction: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -459,262 +600,152 @@ export function registerEVMTools(server: McpServer) { ); // ============================================================================ - // TOKEN INFO TOOLS (Read-only) + // SMART CONTRACT TOOLS // ============================================================================ server.registerTool( - "get_token_info", - { - description: "Get comprehensive information about an ERC20 token including name, symbol, decimals, and total supply", - inputSchema: { - tokenAddress: z.string().describe("The contract address of the ERC20 token"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") - }, - annotations: { - title: "Get Token Info", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: true - } - }, - async ({ tokenAddress, network = "ethereum" }) => { - try { - const tokenInfo = await services.getERC20TokenInfo(tokenAddress as Address, network); - return { - content: [{ type: "text", text: JSON.stringify({ address: tokenAddress, network, ...tokenInfo }, null, 2) }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Error fetching token info: ${error instanceof Error ? error.message : String(error)}` }], - isError: true - }; - } - } - ); - - server.registerTool( - "get_nft_info", - { - description: "Get detailed information about a specific NFT (ERC721 token)", - inputSchema: { - tokenAddress: z.string().describe("The contract address of the NFT collection"), - tokenId: z.string().describe("The ID of the specific NFT token to query"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") - }, - annotations: { - title: "Get NFT Info", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: true - } - }, - async ({ tokenAddress, tokenId, network = "ethereum" }) => { - try { - const nftInfo = await services.getERC721TokenMetadata(tokenAddress as Address, BigInt(tokenId), network); - let owner = null; - try { - owner = await services.getPublicClient(network).readContract({ - address: tokenAddress as Address, - abi: [{ inputs: [{ type: 'uint256' }], name: 'ownerOf', outputs: [{ type: 'address' }], stateMutability: 'view', type: 'function' }], - functionName: 'ownerOf', - args: [BigInt(tokenId)] - }); - } catch (e) { /* Ownership info not available */ } - - return { - content: [{ type: "text", text: JSON.stringify({ contract: tokenAddress, tokenId, network, ...nftInfo, owner: owner || 'Unknown' }, null, 2) }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Error fetching NFT info: ${error instanceof Error ? error.message : String(error)}` }], - isError: true - }; - } - } - ); - - server.registerTool( - "get_erc1155_token_uri", + "get_contract_abi", { - description: "Get the metadata URI for an ERC1155 token", + description: "Fetch a contract's ABI from a block explorer (Etherscan, Polygonscan, etc.). Requires ETHERSCAN_API_KEY environment variable.", inputSchema: { - tokenAddress: z.string().describe("The contract address of the ERC1155 token collection"), - tokenId: z.string().describe("The ID of the specific token to query"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + contractAddress: z.string().describe("The contract address (0x...)"), + network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet. Supported: ethereum, polygon, arbitrum, optimism, base, avalanche, etc.") }, annotations: { - title: "Get ERC1155 Token URI", + title: "Get Contract ABI", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, - async ({ tokenAddress, tokenId, network = "ethereum" }) => { + async ({ contractAddress, network = "ethereum" }) => { try { - const uri = await services.getERC1155TokenURI(tokenAddress as Address, BigInt(tokenId), network); - return { - content: [{ type: "text", text: JSON.stringify({ contract: tokenAddress, tokenId, network, uri }, null, 2) }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Error fetching ERC1155 token URI: ${error instanceof Error ? error.message : String(error)}` }], - isError: true - }; - } - } - ); + const abi = await services.fetchContractABI(contractAddress as Address, network); + const parsed = services.parseABI(abi); + const readableFunctions = services.getReadableFunctions(parsed); - server.registerTool( - "check_nft_ownership", - { - description: "Check if an address owns a specific NFT", - inputSchema: { - tokenAddress: z.string().describe("The contract address of the NFT collection"), - tokenId: z.string().describe("The ID of the NFT to check"), - ownerAddress: z.string().describe("The wallet address to check ownership against"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") - }, - annotations: { - title: "Check NFT Ownership", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: true - } - }, - async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { - try { - const isOwner = await services.isNFTOwner(tokenAddress, ownerAddress, BigInt(tokenId), network); return { content: [{ type: "text", text: JSON.stringify({ - tokenAddress, tokenId, ownerAddress, network, isOwner, - result: isOwner ? "Address owns this NFT" : "Address does not own this NFT" + contractAddress, + network, + abiFormat: "json", + readableFunctions, + totalFunctions: parsed.filter(i => i.type === 'function').length, + abi: parsed }, null, 2) }] }; } catch (error) { return { - content: [{ type: "text", text: `Error checking NFT ownership: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error fetching ABI: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // ============================================================================ - // CONTRACT TOOLS (Read-only) - // ============================================================================ - server.registerTool( "read_contract", { - description: "Read data from a smart contract by calling a view/pure function. This doesn't modify blockchain state.", + description: "Read data from a smart contract (read-only call). Can auto-fetch verified contract ABIs or use common function signatures.", inputSchema: { - contractAddress: z.string().describe("The address of the smart contract"), - abi: z.array(z.any()).describe("The ABI of the smart contract function, as a JSON array"), - functionName: z.string().describe("The name of the function to call"), - args: z.array(z.any()).optional().describe("The arguments to pass to the function"), + contractAddress: z.string().describe("The contract address"), + functionName: z.string().describe("Function name (e.g., 'name', 'symbol', 'balanceOf', 'totalSupply')"), + args: z.array(z.string()).optional().describe("Function arguments as strings"), + abiJson: z.string().optional().describe("Full contract ABI as JSON string (optional - will try to auto-fetch if not provided)"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Read Contract", + title: "Read Smart Contract", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, - async ({ contractAddress, abi, functionName, args = [], network = "ethereum" }) => { + async ({ contractAddress, functionName, args = [], abiJson, network = "ethereum" }) => { try { - const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi; - const params = { + const client = await services.getPublicClient(network); + + let abi: any[] | undefined; + let functionAbi: any; + + // If ABI is provided, use it + if (abiJson) { + try { + abi = services.parseABI(abiJson); + functionAbi = services.getFunctionFromABI(abi, functionName); + } catch (error) { + return { + content: [{ + type: "text", + text: `Error parsing provided ABI: ${error instanceof Error ? error.message : String(error)}` + }], + isError: true + }; + } + } else { + // Try to auto-fetch ABI from block explorer + try { + const fetchedAbi = await services.fetchContractABI(contractAddress as Address, network); + abi = services.parseABI(fetchedAbi); + functionAbi = services.getFunctionFromABI(abi, functionName); + } catch (fetchError) { + // Fall back to common function signatures + const commonFunctions: { [key: string]: any } = { + 'name': { inputs: [], outputs: [{ type: 'string' }] }, + 'symbol': { inputs: [], outputs: [{ type: 'string' }] }, + 'decimals': { inputs: [], outputs: [{ type: 'uint8' }] }, + 'totalSupply': { inputs: [], outputs: [{ type: 'uint256' }] }, + 'balanceOf': { inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }] }, + 'allowance': { inputs: [{ type: 'address' }, { type: 'address' }], outputs: [{ type: 'uint256' }] }, + }; + + if (!commonFunctions[functionName]) { + return { + content: [{ + type: "text", + text: `Error: Could not auto-fetch ABI (${fetchError instanceof Error ? fetchError.message : String(fetchError)}). Function '${functionName}' not in common signatures. Use get_contract_abi to fetch and provide the full ABI, or provide abiJson parameter.` + }], + isError: true + }; + } + + functionAbi = { + name: functionName, + type: 'function', + inputs: commonFunctions[functionName].inputs, + outputs: commonFunctions[functionName].outputs, + stateMutability: 'view' + }; + } + } + + const result = await client.readContract({ address: contractAddress as Address, - abi: parsedAbi, - functionName: functionName as string, - args: args as readonly unknown[] - }; - const result = await services.readContract(params, network as string); - return { content: [{ type: "text", text: services.helpers.formatJson(result) }] }; - } catch (error) { - return { - content: [{ type: "text", text: `Error reading contract: ${error instanceof Error ? error.message : String(error)}` }], - isError: true - }; - } - } - ); + abi: [functionAbi], + functionName: functionName, + args: args as any + }); - server.registerTool( - "is_contract", - { - description: "Check if an address is a smart contract or an externally owned account (EOA)", - inputSchema: { - address: z.string().describe("The address to check"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") - }, - annotations: { - title: "Is Contract", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: true - } - }, - async ({ address, network = "ethereum" }) => { - try { - const isContract = await services.isContract(address, network); return { content: [{ type: "text", text: JSON.stringify({ - address, network, isContract, - type: isContract ? "Contract" : "Externally Owned Account (EOA)" + contractAddress, + function: functionName, + args: args.length > 0 ? args : undefined, + result: result?.toString(), + abiSource: abiJson ? 'provided' : 'auto-fetched or built-in' }, null, 2) }] }; } catch (error) { return { - content: [{ type: "text", text: `Error checking if address is a contract: ${error instanceof Error ? error.message : String(error)}` }], - isError: true - }; - } - } - ); - - // ============================================================================ - // WALLET TOOLS (Read-only) - // ============================================================================ - - server.registerTool( - "get_address_from_private_key", - { - description: "Get the EVM address derived from a private key", - inputSchema: { - privateKey: z.string().describe("Private key in hex format. SECURITY: Used only for address derivation and is not stored.") - }, - annotations: { - title: "Get Address from Private Key", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false - } - }, - async ({ privateKey }) => { - try { - const formattedKey = privateKey.startsWith('0x') ? privateKey as Hex : `0x${privateKey}` as Hex; - const address = services.getAddressFromPrivateKey(formattedKey); - return { - content: [{ type: "text", text: JSON.stringify({ address }, null, 2) }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Error deriving address: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error reading contract: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -722,17 +753,16 @@ export function registerEVMTools(server: McpServer) { ); // ============================================================================ - // TRANSFER TOOLS (Destructive - modify blockchain state) + // TRANSFER TOOLS (Write operations) // ============================================================================ server.registerTool( - "transfer_eth", + "transfer_native", { - description: "Transfer native tokens (ETH, MATIC, etc.) to an address. This modifies blockchain state.", + description: "Transfer native tokens (ETH, MATIC, etc.) to an address. Uses the configured wallet.", inputSchema: { - privateKey: z.string().describe("Private key of the sender. SECURITY: Used only for signing and is not stored."), - to: z.string().describe("The recipient address or ENS name"), - amount: z.string().describe("Amount to send in ether (e.g., '0.1')"), + to: z.string().describe("Recipient address or ENS name"), + amount: z.string().describe("Amount to send in ether (e.g., '0.5' for 0.5 ETH)"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { @@ -743,56 +773,26 @@ export function registerEVMTools(server: McpServer) { openWorldHint: true } }, - async ({ privateKey, to, amount, network = "ethereum" }) => { - try { - const txHash = await services.transferETH(privateKey, to, amount, network); - return { - content: [{ type: "text", text: JSON.stringify({ success: true, txHash, to, amount, network }, null, 2) }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Error transferring: ${error instanceof Error ? error.message : String(error)}` }], - isError: true - }; - } - } - ); - - server.registerTool( - "transfer_erc20", - { - description: "Transfer ERC20 tokens to another address. This modifies blockchain state.", - inputSchema: { - privateKey: z.string().describe("Private key of the sender. SECURITY: Used only for signing and is not stored."), - tokenAddress: z.string().describe("The ERC20 token contract address"), - toAddress: z.string().describe("The recipient address"), - amount: z.string().describe("Amount of tokens to send (e.g., '10' for 10 tokens)"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") - }, - annotations: { - title: "Transfer ERC20 Tokens", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: false, - openWorldHint: true - } - }, - async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { + async ({ to, amount, network = "ethereum" }) => { try { - const formattedKey = privateKey.startsWith('0x') ? privateKey as `0x${string}` : `0x${privateKey}` as `0x${string}`; - const result = await services.transferERC20(tokenAddress as Address, toAddress as Address, amount, formattedKey, network); + const wallet = getConfiguredWallet(); + const txHash = await services.transferETH(wallet, to as Address, amount, network); return { content: [{ type: "text", text: JSON.stringify({ - success: true, txHash: result.txHash, network, tokenAddress, - recipient: toAddress, amount: result.amount.formatted, symbol: result.token.symbol + network, + from: wallet.address, + to, + amount, + txHash, + message: "Transaction sent. Use get_transaction_receipt to check confirmation." }, null, 2) }] }; } catch (error) { return { - content: [{ type: "text", text: `Error transferring ERC20: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error transferring native tokens: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -800,39 +800,44 @@ export function registerEVMTools(server: McpServer) { ); server.registerTool( - "transfer_token", + "transfer_erc20", { - description: "Transfer ERC20 tokens to an address (alias for transfer_erc20)", + description: "Transfer ERC20 tokens to an address. Uses the configured wallet.", inputSchema: { - privateKey: z.string().describe("Private key of the sender. SECURITY: Used only for signing."), tokenAddress: z.string().describe("The ERC20 token contract address"), - toAddress: z.string().describe("The recipient address or ENS name"), - amount: z.string().describe("Amount of tokens to send"), + to: z.string().describe("Recipient address or ENS name"), + amount: z.string().describe("Amount to send (in token units, accounting for decimals)"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Transfer Token", + title: "Transfer ERC20 Tokens", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true } }, - async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { + async ({ tokenAddress, to, amount, network = "ethereum" }) => { try { - const result = await services.transferERC20(tokenAddress, toAddress, amount, privateKey, network); + const wallet = getConfiguredWallet(); + const txHash = await services.transferERC20(wallet, tokenAddress as Address, to as Address, amount, network); return { content: [{ type: "text", text: JSON.stringify({ - success: true, txHash: result.txHash, tokenAddress, toAddress, - amount: result.amount.formatted, symbol: result.token.symbol, network + network, + tokenAddress, + from: wallet.address, + to, + amount, + txHash, + message: "Transaction sent. Use get_transaction_receipt to check confirmation." }, null, 2) }] }; } catch (error) { return { - content: [{ type: "text", text: `Error transferring tokens: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error transferring ERC20 tokens: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -840,82 +845,87 @@ export function registerEVMTools(server: McpServer) { ); server.registerTool( - "transfer_nft", + "approve_token_spending", { - description: "Transfer an NFT (ERC721 token) to another address. This modifies blockchain state.", + description: "Approve a spender (contract) to spend tokens on your behalf. Required before interacting with DEXes, lending protocols, etc.", inputSchema: { - privateKey: z.string().describe("Private key of the NFT owner. SECURITY: Used only for signing."), - tokenAddress: z.string().describe("The NFT collection contract address"), - tokenId: z.string().describe("The ID of the NFT to transfer"), - toAddress: z.string().describe("The recipient address"), + tokenAddress: z.string().describe("The ERC20 token contract address"), + spenderAddress: z.string().describe("The address that will be allowed to spend tokens (usually a contract)"), + amount: z.string().describe("Amount to approve (in token units). Use '0' to revoke approval."), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Transfer NFT", + title: "Approve Token Spending", readOnlyHint: false, - destructiveHint: true, + destructiveHint: false, idempotentHint: false, openWorldHint: true } }, - async ({ privateKey, tokenAddress, tokenId, toAddress, network = "ethereum" }) => { + async ({ tokenAddress, spenderAddress, amount, network = "ethereum" }) => { try { - const formattedKey = privateKey.startsWith('0x') ? privateKey as `0x${string}` : `0x${privateKey}` as `0x${string}`; - const result = await services.transferERC721(tokenAddress as Address, toAddress as Address, BigInt(tokenId), formattedKey, network); + const wallet = getConfiguredWallet(); + const txHash = await services.approveERC20(wallet, tokenAddress as Address, spenderAddress as Address, amount, network); return { content: [{ type: "text", text: JSON.stringify({ - success: true, txHash: result.txHash, network, collection: tokenAddress, - tokenId: result.tokenId, recipient: toAddress, name: result.token.name, symbol: result.token.symbol + network, + tokenAddress, + spender: spenderAddress, + approvalAmount: amount, + txHash, + message: "Approval transaction sent. Use get_transaction_receipt to check confirmation." }, null, 2) }] }; } catch (error) { return { - content: [{ type: "text", text: `Error transferring NFT: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error approving token spending: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); + // ============================================================================ + // NFT TOOLS (Read-only) + // ============================================================================ + server.registerTool( - "transfer_erc1155", + "get_nft_info", { - description: "Transfer ERC1155 tokens to another address. This modifies blockchain state.", + description: "Get information about an ERC721 NFT including metadata URI", inputSchema: { - privateKey: z.string().describe("Private key of the token owner. SECURITY: Used only for signing."), - tokenAddress: z.string().describe("The ERC1155 token collection contract address"), - tokenId: z.string().describe("The ID of the token to transfer"), - amount: z.string().describe("The quantity of tokens to send"), - toAddress: z.string().describe("The recipient address"), + contractAddress: z.string().describe("The NFT contract address"), + tokenId: z.string().describe("The NFT token ID"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Transfer ERC1155 Tokens", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: false, + title: "Get NFT Info", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, openWorldHint: true } }, - async ({ privateKey, tokenAddress, tokenId, amount, toAddress, network = "ethereum" }) => { + async ({ contractAddress, tokenId, network = "ethereum" }) => { try { - const formattedKey = privateKey.startsWith('0x') ? privateKey as `0x${string}` : `0x${privateKey}` as `0x${string}`; - const result = await services.transferERC1155(tokenAddress as Address, toAddress as Address, BigInt(tokenId), amount, formattedKey, network); + const nftInfo = await services.getERC721TokenMetadata(contractAddress as Address, BigInt(tokenId), network); return { content: [{ type: "text", text: JSON.stringify({ - success: true, txHash: result.txHash, network, contract: tokenAddress, - tokenId: result.tokenId, amount: result.amount, recipient: toAddress + network, + contract: contractAddress, + tokenId, + ...nftInfo }, null, 2) }] }; } catch (error) { return { - content: [{ type: "text", text: `Error transferring ERC1155: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error fetching NFT info: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } @@ -923,85 +933,41 @@ export function registerEVMTools(server: McpServer) { ); server.registerTool( - "approve_token_spending", + "get_erc1155_balance", { - description: "Approve another address to spend your ERC20 tokens. This modifies blockchain state.", + description: "Get ERC1155 token balance for an address", inputSchema: { - privateKey: z.string().describe("Private key of the token owner. SECURITY: Used only for signing."), - tokenAddress: z.string().describe("The ERC20 token contract address"), - spenderAddress: z.string().describe("The address being approved to spend tokens"), - amount: z.string().describe("The amount of tokens to approve"), + contractAddress: z.string().describe("The ERC1155 contract address"), + tokenId: z.string().describe("The token ID"), + address: z.string().describe("The owner address or ENS name"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { - title: "Approve Token Spending", - readOnlyHint: false, - destructiveHint: true, + title: "Get ERC1155 Balance", + readOnlyHint: true, + destructiveHint: false, idempotentHint: true, openWorldHint: true } }, - async ({ privateKey, tokenAddress, spenderAddress, amount, network = "ethereum" }) => { + async ({ contractAddress, tokenId, address, network = "ethereum" }) => { try { - const formattedKey = privateKey.startsWith('0x') ? privateKey as `0x${string}` : `0x${privateKey}` as `0x${string}`; - const result = await services.approveERC20(tokenAddress as Address, spenderAddress as Address, amount, formattedKey, network); + const balance = await services.getERC1155Balance(contractAddress as Address, address as Address, BigInt(tokenId), network); return { content: [{ type: "text", text: JSON.stringify({ - success: true, txHash: result.txHash, network, tokenAddress, - spender: spenderAddress, amount: result.amount.formatted, symbol: result.token.symbol + network, + contract: contractAddress, + tokenId, + owner: address, + balance: balance.toString() }, null, 2) }] }; } catch (error) { return { - content: [{ type: "text", text: `Error approving: ${error instanceof Error ? error.message : String(error)}` }], - isError: true - }; - } - } - ); - - server.registerTool( - "write_contract", - { - description: "Write data to a smart contract by calling a state-changing function. This modifies blockchain state.", - inputSchema: { - contractAddress: z.string().describe("The smart contract address"), - abi: z.array(z.any()).describe("The ABI of the function, as a JSON array"), - functionName: z.string().describe("The name of the function to call"), - args: z.array(z.any()).describe("The arguments to pass to the function"), - privateKey: z.string().describe("Private key of the sender. SECURITY: Used only for signing."), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") - }, - annotations: { - title: "Write to Smart Contract", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: false, - openWorldHint: true - } - }, - async ({ contractAddress, abi, functionName, args, privateKey, network = "ethereum" }) => { - try { - const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi; - const contractParams: Record = { - address: contractAddress as Address, - abi: parsedAbi, - functionName, - args - }; - const txHash = await services.writeContract(privateKey as Hex, contractParams, network); - return { - content: [{ - type: "text", - text: JSON.stringify({ network, transactionHash: txHash, message: "Contract write transaction sent successfully" }, null, 2) - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Error writing to contract: ${error instanceof Error ? error.message : String(error)}` }], + content: [{ type: "text", text: `Error fetching ERC1155 balance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } From 3b7bac33ca3852d93a6829a3b5f2524789efe7ed Mon Sep 17 00:00:00 2001 From: Valentin Cart Date: Wed, 19 Nov 2025 11:47:41 -0500 Subject: [PATCH 03/11] feat: Refactor ABI fetching to use a unified Etherscan v2 API and enhance transaction prompts with detailed, safety-focused instructions. --- src/core/services/abi.ts | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/src/core/services/abi.ts b/src/core/services/abi.ts index d61368a..d4e7757 100644 --- a/src/core/services/abi.ts +++ b/src/core/services/abi.ts @@ -1,11 +1,12 @@ import { type Address } from 'viem'; +import { resolveChainId, getSupportedNetworks } from '../chains.js'; /** - * Fetch contract ABI from Etherscan API + * Fetch contract ABI from Etherscan v2 API (unified endpoint for all EVM chains) * Requires ETHERSCAN_API_KEY environment variable to be set * * @param contractAddress The contract address to fetch ABI for - * @param network The network to use (etherscan, polygonscan, etc.) + * @param network The network name or chain ID * @returns The contract ABI as a JSON string */ export async function fetchContractABI( @@ -14,35 +15,25 @@ export async function fetchContractABI( ): Promise { const apiKey = process.env.ETHERSCAN_API_KEY; if (!apiKey) { - throw new Error('ETHERSCAN_API_KEY environment variable is not set. Set it to fetch contract ABIs.'); + throw new Error('ETHERSCAN_API_KEY environment variable is not set. Set it to fetch contract ABIs from block explorers.'); } - // Map network names to Etherscan-compatible API domains - const networkMap: { [key: string]: string } = { - 'ethereum': 'https://api.etherscan.io', - 'sepolia': 'https://api-sepolia.etherscan.io', - 'polygon': 'https://api.polygonscan.com', - 'mumbai': 'https://api-testnet.polygonscan.com', - 'arbitrum': 'https://api.arbiscan.io', - 'arbitrum-sepolia': 'https://api-sepolia.arbiscan.io', - 'optimism': 'https://api-optimistic.etherscan.io', - 'optimism-sepolia': 'https://api-sepolia-optimistic.etherscan.io', - 'base': 'https://api.basescan.org', - 'base-sepolia': 'https://api-sepolia.basescan.org', - 'avalanche': 'https://api.snowtrace.io', - 'avalanche-fuji': 'https://api-testnet.snowtrace.io', - }; - - const apiUrl = networkMap[network.toLowerCase()]; - if (!apiUrl) { - throw new Error(`Network "${network}" is not supported for ABI fetching. Supported: ${Object.keys(networkMap).join(', ')}`); + // Resolve chain ID using the chains.ts utilities + let chainId: number; + try { + chainId = resolveChainId(network); + } catch (error) { + const supported = getSupportedNetworks(); + throw new Error(`Network "${network}" is not supported. Supported: ${supported.join(', ')}`); } try { - const url = new URL(`${apiUrl}/api`); + // Use unified Etherscan v2 API endpoint + const url = new URL('https://api.etherscan.io/v2/api'); url.searchParams.set('module', 'contract'); url.searchParams.set('action', 'getabi'); url.searchParams.set('address', contractAddress); + url.searchParams.set('chainid', chainId.toString()); url.searchParams.set('apikey', apiKey); const response = await fetch(url.toString()); From 796245bb7fea9a38fdd2b83cdb0702fc6b506c32 Mon Sep 17 00:00:00 2001 From: Valentin Cart Date: Wed, 19 Nov 2025 11:58:42 -0500 Subject: [PATCH 04/11] feat: updated docs --- README.md | 153 +++++++++++++++++++++++++++++++++++++++------------ package.json | 4 +- 2 files changed, 120 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index bf74039..4a6e413 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # EVM MCP Server ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) -![EVM Networks](https://img.shields.io/badge/Networks-30+-green) -![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6) -![Viem](https://img.shields.io/badge/Viem-1.0+-green) +![EVM Networks](https://img.shields.io/badge/Networks-60+-green) +![TypeScript](https://img.shields.io/badge/TypeScript-5.8+-3178C6) +![MCP](https://img.shields.io/badge/MCP-1.22.0+-blue) +![Viem](https://img.shields.io/badge/Viem-2.39.3+-green) -A comprehensive Model Context Protocol (MCP) server that provides blockchain services across multiple EVM-compatible networks. This server enables AI agents to interact with Ethereum, Optimism, Arbitrum, Base, Polygon, and many other EVM chains with a unified interface. +A comprehensive Model Context Protocol (MCP) server that provides blockchain services across 60+ EVM-compatible networks. This server enables AI agents to interact with Ethereum, Optimism, Arbitrum, Base, Polygon, and many other EVM chains with a unified interface through 21 tools and 9 AI-guided prompts. ## πŸ“‹ Contents @@ -14,10 +15,13 @@ A comprehensive Model Context Protocol (MCP) server that provides blockchain ser - [Supported Networks](#supported-networks) - [Prerequisites](#prerequisites) - [Installation](#installation) -- [Server Configuration](#server-configuration) +- [Configuration](#configuration) + - [Environment Variables](#environment-variables) + - [Server Configuration](#server-configuration) - [Usage](#usage) - [API Reference](#api-reference) - [Tools](#tools) + - [Prompts](#prompts) - [Resources](#resources) - [Security Considerations](#security-considerations) - [Project Structure](#project-structure) @@ -29,19 +33,20 @@ A comprehensive Model Context Protocol (MCP) server that provides blockchain ser The MCP EVM Server leverages the Model Context Protocol to provide blockchain services to AI agents. It supports a wide range of services including: - Reading blockchain state (balances, transactions, blocks, etc.) -- Interacting with smart contracts +- Interacting with smart contracts with **automatic ABI fetching** from block explorers - Transferring tokens (native, ERC20, ERC721, ERC1155) - Querying token metadata and balances -- Chain-specific services across 30+ EVM networks +- Chain-specific services across 60+ EVM networks (34 mainnets + 26 testnets) - **ENS name resolution** for all address parameters (use human-readable names like 'vitalik.eth' instead of addresses) +- **AI-friendly prompts** that guide agents through complex workflows -All services are exposed through a consistent interface of MCP tools and resources, making it easy for AI agents to discover and use blockchain functionality. **Every tool that accepts Ethereum addresses also supports ENS names**, automatically resolving them to addresses behind the scenes. +All services are exposed through a consistent interface of MCP tools, resources, and prompts, making it easy for AI agents to discover and use blockchain functionality. **Every tool that accepts Ethereum addresses also supports ENS names**, automatically resolving them to addresses behind the scenes. The server includes intelligent ABI fetching, eliminating the need to know contract ABIs in advance. ## ✨ Features ### Blockchain Data Access -- **Multi-chain support** for 30+ EVM-compatible networks +- **Multi-chain support** for 60+ EVM-compatible networks (34 mainnets + 26 testnets) - **Chain information** including blockNumber, chainId, and RPCs - **Block data** access by number, hash, or latest - **Transaction details** and receipts with decoded logs @@ -73,6 +78,8 @@ All services are exposed through a consistent interface of MCP tools and resourc - **Write services** with private key signing - **Contract verification** to distinguish from EOAs - **Event logs** retrieval and filtering +- **Automatic ABI fetching** from Etherscan v2 API across all 60+ networks (no need to know ABIs in advance) +- **ABI parsing and validation** with function discovery ### Comprehensive Transaction Support @@ -81,6 +88,15 @@ All services are exposed through a consistent interface of MCP tools and resourc - **Transaction status** and receipt information - **Error handling** with descriptive messages +### AI-Guided Workflows (Prompts) + +- **Transaction preparation** - Guidance for planning and executing transfers +- **Wallet analysis** - Tools for analyzing wallet activity and holdings +- **Smart contract exploration** - Interactive ABI fetching and contract analysis +- **Network information** - Learning about EVM networks and comparisons +- **Approval auditing** - Reviewing and managing token approvals +- **Error diagnosis** - Troubleshooting transaction failures + ## 🌐 Supported Networks ### Mainnets @@ -144,8 +160,9 @@ All services are exposed through a consistent interface of MCP tools and resourc ## πŸ› οΈ Prerequisites -- [Bun](https://bun.sh/) 1.0.0 or higher -- Node.js 18.0.0 or higher (if not using Bun) +- [Bun](https://bun.sh/) 1.0.0 or higher (recommended) +- Node.js 20.0.0 or higher (if not using Bun) +- Optional: [Etherscan API key](https://etherscan.io/apis) for ABI fetching ## πŸ“¦ Installation @@ -161,7 +178,41 @@ bun install npm install ``` -## βš™οΈ Server Configuration +## βš™οΈ Configuration + +### Environment Variables + +The server uses the following environment variables. For write operations and ABI fetching, you must configure these variables: + +#### Private Key (For Write Operations) + +```bash +export EVM_PRIVATE_KEY="0x..." # Your private key in hex format (with or without 0x prefix) +``` + +This private key is used for: +- Transferring native tokens (`transfer_native` tool) +- Transferring ERC20 tokens (`transfer_erc20` tool) +- Approving token spending (`approve_token_spending` tool) + +⚠️ **Security**: Never commit your private key to version control. Use environment variables or a secure key management system. + +#### API Keys (For ABI Fetching) + +```bash +export ETHERSCAN_API_KEY="your-api-key-here" +``` + +This API key is optional but required for: +- Automatic ABI fetching from block explorers (`get_contract_abi` tool) +- Auto-fetching ABIs when reading contracts (`read_contract` tool with `abiJson` parameter) +- The `fetch_and_analyze_abi` prompt + +Get your free API key from: +- [Etherscan](https://etherscan.io/apis) - For Ethereum and compatible chains +- The same key works across all 60+ EVM networks via the Etherscan v2 API + +### Server Configuration The server uses the following default configuration: @@ -389,36 +440,68 @@ console.log(result); ### Tools -The server provides the following MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.** +The server provides 21 focused MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.** + +#### Wallet Information + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `get_wallet_address` | Get the address of the configured wallet (from EVM_PRIVATE_KEY) | none | + +#### Network Information + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `get_chain_info` | Get network information | `network` | +| `get_supported_networks` | List all supported EVM networks | none | +| `get_gas_price` | Get current gas prices on a network | `network` | -#### Token services +#### ENS Services | Tool Name | Description | Key Parameters | |-----------|-------------|----------------| -| `get-token-info` | Get ERC20 token metadata | `tokenAddress` (address/ENS), `network` | -| `get-token-balance` | Check ERC20 token balance | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `network` | -| `transfer-token` | Transfer ERC20 tokens | `privateKey`, `tokenAddress` (address/ENS), `toAddress` (address/ENS), `amount`, `network` | -| `approve-token-spending` | Approve token allowances | `privateKey`, `tokenAddress` (address/ENS), `spenderAddress` (address/ENS), `amount`, `network` | -| `get-nft-info` | Get NFT metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | -| `check-nft-ownership` | Verify NFT ownership | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | -| `transfer-nft` | Transfer an NFT | `privateKey`, `tokenAddress` (address/ENS), `tokenId`, `toAddress` (address/ENS), `network` | -| `get-nft-balance` | Count NFTs owned | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `network` | -| `get-erc1155-token-uri` | Get ERC1155 metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | -| `get-erc1155-balance` | Check ERC1155 balance | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | -| `transfer-erc1155` | Transfer ERC1155 tokens | `privateKey`, `tokenAddress` (address/ENS), `tokenId`, `amount`, `toAddress` (address/ENS), `network` | - -#### Blockchain services +| `resolve_ens_name` | Resolve ENS name to address | `ensName`, `network` | +| `lookup_ens_address` | Reverse lookup address to ENS name | `address`, `network` | + +#### Block & Transaction Information + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `get_block` | Get block data | `blockNumber` or `blockHash`, `network` | +| `get_latest_block` | Get latest block data | `network` | +| `get_transaction` | Get transaction details | `txHash`, `network` | +| `get_transaction_receipt` | Get transaction receipt with logs | `txHash`, `network` | +| `wait_for_transaction` | Wait for transaction confirmation | `txHash`, `confirmations`, `network` | + +#### Balance & Token Information + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `get_balance` | Get native token balance | `address` (address/ENS), `network` | +| `get_token_balance` | Check ERC20 token balance | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `network` | +| `get_allowance` | Check token spending allowance | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `spenderAddress` (address/ENS), `network` | + +#### Smart Contract Interactions + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `get_contract_abi` | Fetch contract ABI from block explorer (60+ networks) | `contractAddress` (address/ENS), `network` | +| `read_contract` | Read smart contract state (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `abiJson` (optional), `network` | + +#### Token Transfers + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `transfer_native` | Send native tokens (ETH, etc.) | `to` (address/ENS), `amount`, `network` | +| `transfer_erc20` | Transfer ERC20 tokens | `tokenAddress` (address/ENS), `to` (address/ENS), `amount`, `network` | +| `approve_token_spending` | Approve token allowances | `tokenAddress` (address/ENS), `spenderAddress` (address/ENS), `amount`, `network` | + +#### NFT Services | Tool Name | Description | Key Parameters | |-----------|-------------|----------------| -| `get-chain-info` | Get network information | `network` | -| `get-balance` | Get native token balance | `address` (address/ENS), `network` | -| `transfer-eth` | Send native tokens | `privateKey`, `to` (address/ENS), `amount`, `network` | -| `get-transaction` | Get transaction details | `txHash`, `network` | -| `read-contract` | Read smart contract state | `contractAddress` (address/ENS), `abi`, `functionName`, `args`, `network` | -| `write-contract` | Write to smart contract | `contractAddress` (address/ENS), `abi`, `functionName`, `args`, `privateKey`, `network` | -| `is-contract` | Check if address is a contract | `address` (address/ENS), `network` | -| `resolve-ens` | Resolve ENS name to address | `ensName`, `network` | +| `get_nft_info` | Get NFT (ERC721) metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | +| `get_erc1155_balance` | Check ERC1155 balance | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | ### Resources diff --git a/package.json b/package.json index 294ba07..743614b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "module": "src/index.ts", "type": "module", "version": "2.0.0", - "description": "MCP server for interacting with EVM-compatible blockchains - supports 28 tools across 86+ networks", + "description": "MCP server for interacting with EVM-compatible blockchains - supports 21 tools and 9 prompts across 60+ networks", "bin": { "evm-mcp-server": "./bin/cli.js" }, @@ -70,4 +70,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file From 554ac8ce2cd90fe4bead6aa9b7c51a9e9bd0bebc Mon Sep 17 00:00:00 2001 From: Valentin Cart Date: Wed, 19 Nov 2025 12:00:49 -0500 Subject: [PATCH 05/11] feat: enhance prompt content and structure for token transfers, transaction diagnosis, and wallet analysis --- src/core/tools.ts | 63 ++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/src/core/tools.ts b/src/core/tools.ts index 24c1eeb..de4a269 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -3,8 +3,8 @@ import { z } from "zod"; import { getSupportedNetworks, getRpcUrl } from "./chains.js"; import * as services from "./services/index.js"; import { type Address, type Hex, type Hash } from 'viem'; -import { normalize } from 'viem/ens'; import { privateKeyToAccount } from 'viem/accounts'; +import { normalize } from 'viem/ens'; /** * Register all EVM-related tools with the MCP server @@ -19,13 +19,26 @@ import { privateKeyToAccount } from 'viem/accounts'; * @param server The MCP server instance */ export function registerEVMTools(server: McpServer) { - // Helper to get the configured wallet from environment - const getConfiguredWallet = () => { + // Helper to get the configured private key from environment + const getConfiguredPrivateKey = (): Hex => { const privateKey = process.env.EVM_PRIVATE_KEY; if (!privateKey) { throw new Error("EVM_PRIVATE_KEY environment variable is not set. Configure it to enable write operations."); } - return privateKeyToAccount(privateKey as Hex); + // Ensure 0x prefix + return (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`) as Hex; + }; + + // Helper to get wallet address from private key + const getWalletAddressFromKey = (): Address => { + const privateKey = getConfiguredPrivateKey(); + const account = privateKeyToAccount(privateKey); + return account.address; + }; + + // Helper to get configured wallet object + const getConfiguredWallet = (): { address: Address } => { + return { address: getWalletAddressFromKey() }; }; // ============================================================================ @@ -47,12 +60,12 @@ export function registerEVMTools(server: McpServer) { }, async () => { try { - const wallet = getConfiguredWallet(); + const address = getWalletAddressFromKey(); return { content: [{ type: "text", text: JSON.stringify({ - address: wallet.address, + address, message: "This is the wallet that will be used for all transactions" }, null, 2) }] @@ -606,10 +619,10 @@ export function registerEVMTools(server: McpServer) { server.registerTool( "get_contract_abi", { - description: "Fetch a contract's ABI from a block explorer (Etherscan, Polygonscan, etc.). Requires ETHERSCAN_API_KEY environment variable.", + description: "Fetch a contract's full ABI from Etherscan/block explorers. Use this to understand verified contracts before interacting. Requires ETHERSCAN_API_KEY. Supports 30+ EVM networks. Works best with verified contracts on block explorers.", inputSchema: { contractAddress: z.string().describe("The contract address (0x...)"), - network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet. Supported: ethereum, polygon, arbitrum, optimism, base, avalanche, etc.") + network: z.string().optional().describe("Network name or chain ID. Defaults to ethereum. Supported: ethereum, polygon, arbitrum, optimism, base, avalanche, gnosis, fantom, bsc, celo, scroll, linea, zksync, manta, blast, and testnets (sepolia, mumbai, arbitrum-sepolia, optimism-sepolia, base-sepolia, avalanche-fuji)") }, annotations: { title: "Get Contract ABI", @@ -650,12 +663,12 @@ export function registerEVMTools(server: McpServer) { server.registerTool( "read_contract", { - description: "Read data from a smart contract (read-only call). Can auto-fetch verified contract ABIs or use common function signatures.", + description: "Call read-only functions on a smart contract. Automatically fetches ABI from block explorer if not provided (requires ETHERSCAN_API_KEY). Falls back to common functions if contract is not verified. Use this to query contract state and data.", inputSchema: { contractAddress: z.string().describe("The contract address"), - functionName: z.string().describe("Function name (e.g., 'name', 'symbol', 'balanceOf', 'totalSupply')"), - args: z.array(z.string()).optional().describe("Function arguments as strings"), - abiJson: z.string().optional().describe("Full contract ABI as JSON string (optional - will try to auto-fetch if not provided)"), + functionName: z.string().describe("Function name (e.g., 'name', 'symbol', 'balanceOf', 'totalSupply', 'owner')"), + args: z.array(z.string()).optional().describe("Function arguments as strings (e.g., ['0xAddress'] for balanceOf)"), + abiJson: z.string().optional().describe("Full contract ABI as JSON string (optional - will auto-fetch verified contract ABI if not provided)"), network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, annotations: { @@ -775,14 +788,15 @@ export function registerEVMTools(server: McpServer) { }, async ({ to, amount, network = "ethereum" }) => { try { - const wallet = getConfiguredWallet(); - const txHash = await services.transferETH(wallet, to as Address, amount, network); + const privateKey = getConfiguredPrivateKey(); + const senderAddress = getWalletAddressFromKey(); + const txHash = await services.transferETH(privateKey, to as Address, amount, network); return { content: [{ type: "text", text: JSON.stringify({ network, - from: wallet.address, + from: senderAddress, to, amount, txHash, @@ -819,18 +833,21 @@ export function registerEVMTools(server: McpServer) { }, async ({ tokenAddress, to, amount, network = "ethereum" }) => { try { - const wallet = getConfiguredWallet(); - const txHash = await services.transferERC20(wallet, tokenAddress as Address, to as Address, amount, network); + const privateKey = getConfiguredPrivateKey(); + const senderAddress = getWalletAddressFromKey(); + const result = await services.transferERC20(tokenAddress as Address, to as Address, amount, privateKey, network); return { content: [{ type: "text", text: JSON.stringify({ network, tokenAddress, - from: wallet.address, + from: senderAddress, to, - amount, - txHash, + amount: result.amount.formatted, + symbol: result.token.symbol, + decimals: result.token.decimals, + txHash: result.txHash, message: "Transaction sent. Use get_transaction_receipt to check confirmation." }, null, 2) }] @@ -864,14 +881,16 @@ export function registerEVMTools(server: McpServer) { }, async ({ tokenAddress, spenderAddress, amount, network = "ethereum" }) => { try { - const wallet = getConfiguredWallet(); - const txHash = await services.approveERC20(wallet, tokenAddress as Address, spenderAddress as Address, amount, network); + const privateKey = getConfiguredPrivateKey(); + const senderAddress = getWalletAddressFromKey(); + const txHash = await services.approveERC20(privateKey, tokenAddress as Address, spenderAddress as Address, amount, network); return { content: [{ type: "text", text: JSON.stringify({ network, tokenAddress, + owner: senderAddress, spender: spenderAddress, approvalAmount: amount, txHash, From 7fdc5c2cd7cb806ff73ec024e2db73b21e151a08 Mon Sep 17 00:00:00 2001 From: Valentin Cart Date: Wed, 19 Nov 2025 12:01:45 -0500 Subject: [PATCH 06/11] refactor: update prompt definitions with detailed, structured markdown content for token transfer, transaction diagnosis, and wallet analysis. --- src/core/prompts.ts | 901 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 735 insertions(+), 166 deletions(-) diff --git a/src/core/prompts.ts b/src/core/prompts.ts index eb8a01c..63f4c70 100644 --- a/src/core/prompts.ts +++ b/src/core/prompts.ts @@ -4,27 +4,32 @@ import { z } from "zod"; /** * Register task-oriented prompts with the MCP server * - * Prompts function like macros - they guide the model through complex multi-step workflows. - * Instead of the AI agent having to discover and call multiple tools in the right sequence, - * these prompts provide a structured approach to common blockchain tasks. + * All prompts follow a consistent structure: + * - Clear objective statement + * - Step-by-step instructions + * - Expected outputs + * - Safety/security considerations + * + * Prompts guide the model through complex workflows that would otherwise + * require multiple tool calls in the correct sequence. * * @param server The MCP server instance */ export function registerEVMPrompts(server: McpServer) { // ============================================================================ - // TRANSACTION PREPARATION PROMPTS + // TRANSACTION PROMPTS // ============================================================================ server.registerPrompt( "prepare_transfer", { - description: "Guide through preparing a token transfer with safety checks", + description: "Safely prepare and execute a token transfer with validation checks", argsSchema: { - tokenType: z.enum(["native", "erc20"]).describe("Type of token: 'native' for ETH/MATIC, 'erc20' for contract tokens"), + tokenType: z.enum(["native", "erc20"]).describe("Token type: 'native' for ETH/MATIC or 'erc20' for contract tokens"), recipient: z.string().describe("Recipient address or ENS name"), - amount: z.string().describe("Amount to transfer"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'polygon'). Defaults to Ethereum mainnet."), - tokenAddress: z.string().optional().describe("Token contract address (required for ERC20 transfers)") + amount: z.string().describe("Amount to transfer (in ether for native, token units for ERC20)"), + network: z.string().optional().describe("Network name (default: ethereum)"), + tokenAddress: z.string().optional().describe("Token contract address (required for ERC20)") } }, ({ tokenType, recipient, amount, network = "ethereum", tokenAddress }) => ({ @@ -32,31 +37,49 @@ export function registerEVMPrompts(server: McpServer) { role: "user", content: { type: "text", - text: tokenType === "native" - ? `I want to transfer ${amount} native tokens (${network === "ethereum" ? "ETH" : "MATIC"}) to ${recipient} on ${network}. - -Before proceeding: -1. Call get_wallet_address to confirm which wallet will send the transaction -2. Call get_balance to verify I have enough balance -3. Call get_gas_price to understand current gas costs -4. Summarize the transaction details and ask for confirmation before executing -5. Call transfer_native once approved -6. Wait for confirmation with wait_for_transaction - -Be clear about gas costs and the impact of this transaction.` - : `I want to transfer ${amount} tokens (contract: ${tokenAddress}) to ${recipient} on ${network}. - -Before proceeding: -1. Call get_wallet_address to confirm which wallet will send the transaction -2. Call get_token_balance to verify I have enough balance -3. Call get_gas_price to understand current gas costs -4. Check if an approval is needed (call get_allowance for your DEX/protocol) -5. Summarize the transaction details and ask for confirmation before executing -6. If approval needed, call approve_token_spending first -7. Call transfer_erc20 once approved -8. Wait for confirmation with wait_for_transaction - -Be clear about gas costs, token decimals, and the impact of this transaction.` + text: `# Token Transfer Task + +**Objective**: Safely transfer ${amount} ${tokenType === "native" ? "native tokens" : "ERC20 tokens"} to ${recipient} on ${network} + +## Validation & Checks +Before executing any transfer: +1. **Wallet Verification**: Call \`get_wallet_address\` to confirm the sending wallet +2. **Balance Check**: + ${tokenType === "native" + ? "- Call `get_balance` to verify native token balance" + : "- Call `get_token_balance` with tokenAddress=${tokenAddress} to verify balance"} +3. **Gas Analysis**: Call \`get_gas_price\` to assess current network costs +${tokenType === "erc20" ? `4. **Approval Check**: Call \`get_allowance\` to verify approval (if needed for protocols)` : ""} + +## Execution Steps +${tokenType === "native" ? ` +1. Summarize: sender address, recipient, amount, and estimated gas cost +2. Request confirmation from user +3. Call \`transfer_native\` with to="${recipient}", amount="${amount}", network="${network}" +4. Return transaction hash to user +5. Call \`wait_for_transaction\` to confirm completion +` : ` +1. Check if approval is needed: + - If allowance < amount: Call \`approve_token_spending\` first + - Then proceed with transfer +2. Summarize: sender, recipient, token, amount, decimals, gas estimate +3. Request confirmation +4. Call \`transfer_erc20\` with tokenAddress, recipient, amount +5. Wait for confirmation with \`wait_for_transaction\` +`} + +## Output Format +- **Transaction Hash**: Clear hex value +- **Status**: Pending or Confirmed +- **Cost Estimate**: Gas price and total cost +- **User Confirmation**: Always ask before sending + +## Safety Considerations +- Never send more than available balance +- Double-check recipient address +- Warn about high gas prices +- Explain any approval requirements +` } }] }) @@ -65,10 +88,10 @@ Be clear about gas costs, token decimals, and the impact of this transaction.` server.registerPrompt( "diagnose_transaction", { - description: "Analyze a failed or pending transaction and suggest solutions", + description: "Analyze transaction status, failures, and provide debugging insights", argsSchema: { - txHash: z.string().describe("Transaction hash (0x...)"), - network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet.") + txHash: z.string().describe("Transaction hash to diagnose (0x...)"), + network: z.string().optional().describe("Network name (default: ethereum)") } }, ({ txHash, network = "ethereum" }) => ({ @@ -76,23 +99,67 @@ Be clear about gas costs, token decimals, and the impact of this transaction.` role: "user", content: { type: "text", - text: `Please diagnose this transaction: ${txHash} on ${network} - -Follow these steps: -1. Call get_transaction to get the transaction details -2. Call get_transaction_receipt to check the status and gas used -3. Analyze what went wrong or what the transaction is doing -4. If it failed: - - Check if it was out of gas - - Check if it was a contract revert - - Look at the gas limit vs gas used -5. Provide: - - Current status (pending/confirmed/failed) - - Why it failed (if applicable) - - Gas analysis - - Recommended next steps - -Be specific about the issues and provide actionable solutions.` + text: `# Transaction Diagnosis + +**Objective**: Analyze transaction ${txHash} on ${network} and identify any issues + +## Investigation Process + +### 1. Gather Transaction Data +- Call \`get_transaction\` to fetch transaction details +- Call \`get_transaction_receipt\` to get status and gas used +- Note: both calls are read-only and free + +### 2. Status Assessment +Determine transaction state: +- **Pending**: Not yet mined (check mempool conditions) +- **Confirmed**: Successfully executed (status='success') +- **Failed**: Execution failed (status='failed') +- **Replaced**: Transaction was dropped/replaced (check nonce) + +### 3. Failure Analysis +If transaction failed, investigate: + +**Out of Gas**: +- Compare gasUsed vs gasLimit in receipt +- If gasUsed >= gasLimit, suggest increasing gas limit + +**Contract Revert**: +- Check function called and parameters +- Verify sufficient balance/approvals +- Look for require/revert statements in contract + +**Invalid Nonce**: +- Compare transaction nonce with account's current nonce +- Suggest pending transactions may need replacement + +**Other Issues**: +- Check sender/recipient addresses are valid +- Verify function parameters are correct type +- Look for access control restrictions + +### 4. Gas Analysis +- Calculate gas cost: gasUsed * gasPrice +- Compare to current gas prices (call \`get_gas_price\`) +- Assess if overpaid or underpaid + +## Output Format + +Provide structured diagnosis: +- **Status**: Pending/Confirmed/Failed with reason +- **Transaction Hash**: The hash analyzed +- **From/To**: Addresses involved +- **Function**: What was called +- **Gas Analysis**: Used vs limit, cost +- **Issue (if failed)**: Root cause and explanation +- **Recommended Actions**: Next steps to resolve + +## Important Notes +- Be specific about error messages and codes +- Provide actionable recommendations +- Link issues to specific contract behavior +- Suggest solutions (retry, increase gas, fix parameters, etc.) +` } }] }) @@ -105,34 +172,76 @@ Be specific about the issues and provide actionable solutions.` server.registerPrompt( "analyze_wallet", { - description: "Get a comprehensive overview of a wallet's assets and activity", + description: "Get comprehensive overview of wallet assets, balances, and activity", argsSchema: { address: z.string().describe("Wallet address or ENS name to analyze"), - network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet."), - tokens: z.string().optional().describe("Comma-separated list of ERC20 token addresses to check balance for") + network: z.string().optional().describe("Network name (default: ethereum)"), + tokens: z.string().optional().describe("Comma-separated token addresses to check") } }, ({ address, network = "ethereum", tokens }) => { const tokenList = tokens ? tokens.split(',').map(t => t.trim()) : []; return { - messages: [{ - role: "user", - content: { - type: "text", - text: `Please analyze the wallet ${address} on ${network}: - -1. Call resolve_ens_name if the input looks like an ENS name, otherwise use it as an address -2. Call get_balance to get the native token balance -3. If tokens are specified, call get_token_balance for each token to show holdings -4. Provide a summary with: - - Wallet address and ENS name (if any) - - Native token balance (in both wei and ether) - - Token holdings (if checked) - - Overall asset overview - -Format the results in a clear, readable way. If the wallet has no balance, let the user know.` - } - }] + messages: [{ + role: "user", + content: { + type: "text", + text: `# Wallet Analysis + +**Objective**: Provide complete asset overview for ${address} on ${network} + +## Information Gathering + +### 1. Address Resolution +- If input contains '.eth', call \`resolve_ens_name\` to get address +- Otherwise use as direct address +- Provide both resolved address and ENS name if applicable + +### 2. Native Token Balance +- Call \`get_balance\` to fetch native token (ETH/MATIC/etc) balance +- Report both wei and ether/human-readable formats +- Note: Free read-only call + +### 3. Token Balances +${tokenList.length > 0 + ? `- Call \`get_token_balance\` for each token:\n${tokenList.map(t => ` * ${t}`).join('\n')}` + : `- If specific tokens provided: call \`get_token_balance\` for each +- Include token symbol and decimals if available`} + +## Output Format + +Provide analysis with clear sections: + +**Wallet Overview** +- Address: [address] +- ENS Name: [name or none] +- Network: [network] + +**Native Token Balance** +- Ether: [formatted amount] +- Wei: [raw amount] +- In USD (if price available): [estimated value] + +**Token Holdings** (if requested) +- Token: [address] +- Symbol: [symbol] +- Balance: [formatted] +- Decimals: [decimals] + +**Summary** +- Total assets value (if prices available) +- Primary holdings +- Notable observations + +## Key Considerations +- Show both formatted and raw amounts +- Include token decimals for precision +- Note if wallet has low/no balance +- Highlight any unusual patterns +- Be clear about what data was available vs not +` + } + }] }; } ); @@ -140,11 +249,11 @@ Format the results in a clear, readable way. If the wallet has no balance, let t server.registerPrompt( "audit_approvals", { - description: "Review token approvals for a wallet and identify security risks", + description: "Review token approvals and identify security risks from unlimited spend", argsSchema: { - address: z.string().optional().describe("Wallet address to audit (defaults to configured wallet)"), - tokenAddress: z.string().describe("ERC20 token contract address to check approvals for"), - network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet.") + address: z.string().optional().describe("Wallet to audit (default: configured wallet)"), + tokenAddress: z.string().describe("Token contract address to check approvals for"), + network: z.string().optional().describe("Network name (default: ethereum)") } }, ({ address, tokenAddress, network = "ethereum" }) => ({ @@ -152,131 +261,512 @@ Format the results in a clear, readable way. If the wallet has no balance, let t role: "user", content: { type: "text", - text: `Please audit token approvals for ${tokenAddress} on ${network}${address ? ` for address ${address}` : " for the configured wallet"}: - -1. If no address is provided, call get_wallet_address to get the configured wallet -2. Call get_allowance to check if there are any existing approvals -3. Analyze the approval amount and what it means: - - If allowance is 0: No approval set - - If allowance is a normal amount: Limited approval (safe) - - If allowance is max uint256: Unlimited approval (security risk) -4. Provide recommendations: - - Is the current approval appropriate? - - Should any dangerous approvals be revoked? - - What approvals should be set up before interacting with protocols? - -Be clear about the security implications of unlimited approvals.` + text: `# Token Approval Audit + +**Objective**: Check and analyze token approvals to identify security risks + +## Approval Analysis + +### 1. Get Configured Wallet (if needed) +- If no address provided: call \`get_wallet_address\` to get the configured wallet +- Use that as the owner for approval checks + +### 2. Check Current Approvals +- Call \`get_allowance\` with: + * tokenAddress: ${tokenAddress} + * ownerAddress: [wallet address from step 1] + * spenderAddress: [contract being analyzed] +- Note the allowance amount returned + +### 3. Interpret Results + +**Allowance = 0** +- No approval set +- User must approve before spender can use tokens +- Safe state + +**Allowance < Max Value** +- Limited approval (safest approach) +- Spender can only use up to this amount +- Tokens are protected + +**Allowance = Max uint256 (unlimited)** +- Dangerous! Spender has unlimited access +- Common but risky pattern +- Should be revoked if not actively used + +## Security Assessment + +For each approval found: +1. **Risk Level**: Low/Medium/High based on: + - Is it unlimited (high risk)? + - How trusted is the spender? + - Is it actively used? + +2. **Recommendations**: + - Revoke unknown/untrusted spenders + - Lower limits on high-risk approvals + - Keep active approvals but monitor + - Remove expired/legacy approvals + +## Output Format + +**Token Approval Audit Report** + +For each spender: +- **Spender Address**: [contract address] +- **Current Allowance**: [amount or "Unlimited"] +- **Risk Level**: Low/Medium/High +- **Status**: Active/Unused +- **Recommendation**: Keep/Reduce/Revoke + +**Summary** +- Total dangerous approvals: [count] +- Recommendations: [action items] +- Overall risk: Safe/Moderate/High + +## Important Notes +- Unlimited approvals are a major attack vector +- Only approve what's necessary +- Regularly audit and revoke unused approvals +- Be especially careful with new/unknown contracts +` } }] }) ); // ============================================================================ - // SMART CONTRACT EXPLORATION PROMPTS + // SMART CONTRACT ANALYSIS PROMPTS // ============================================================================ server.registerPrompt( - "explore_contract", + "fetch_and_analyze_abi", { - description: "Analyze a smart contract to understand its functions and state", + description: "Fetch contract ABI from block explorer and provide comprehensive analysis", argsSchema: { - contractAddress: z.string().describe("The contract address to explore"), - network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet.") + contractAddress: z.string().describe("Contract address to analyze"), + network: z.string().optional().describe("Network name (default: ethereum)"), + findFunction: z.string().optional().describe("Specific function to analyze (e.g., 'swap', 'mint')") } }, - ({ contractAddress, network = "ethereum" }) => ({ + ({ contractAddress, network = "ethereum", findFunction }) => ({ messages: [{ role: "user", content: { type: "text", - text: `Please explore the smart contract at ${contractAddress} on ${network}: - -1. This is a read-only exploration - no transactions will be executed -2. Identify what type of contract this is (token, NFT, DEX, lending, etc.) -3. Try to read common functions that might exist: - - For tokens: name(), symbol(), decimals(), totalSupply() - - For NFTs: name(), symbol(), totalSupply() - - For other contracts: relevant state functions -4. Use read_contract to call these functions -5. Provide a summary with: - - Contract type and purpose (best guess) - - Key properties discovered - - Security notes (if applicable) - - Next steps for interacting with it - -Be clear about what you were able to discover and what remains unknown.` + text: `# ABI Fetch and Analysis + +**Objective**: Retrieve and analyze contract ABI from block explorer + +## Prerequisites +- Contract must be verified on block explorer (Etherscan/Polygonscan/etc) +- ETHERSCAN_API_KEY environment variable required +- Supports 30+ EVM networks via unified Etherscan v2 API +- Read-only, no gas cost + +## Fetching Process + +### 1. Fetch the ABI +- Call \`get_contract_abi\` with contractAddress="${contractAddress}", network="${network}" +- Returns full ABI array with all functions, events, state variables +- Includes metadata about each function (inputs, outputs, mutability) + +### 2. Parse and Categorize +Organize functions by type: + +**View/Pure Functions** (Read-only, free): +- Check current state +- Query data without state change +- Safe to call + +**State-Changing Functions**: +- Payable: require ETH value +- Nonpayable: modify contract state +- Cost gas, need signer + +**Admin Functions**: +- Often restricted (onlyOwner, etc) +- Control contract behavior +- High risk if compromised + +### 3. Analyze Structure +- Count functions by type +- Identify events and their usage +- Look for special functions (constructor, fallback, receive) +- Check for custom errors + +${findFunction ? `### 4. Find Specific Function +- Search for "${findFunction}" in ABI +- Document: inputs, outputs, mutability +- Explain what it does +- Note any access controls` : `### 4. Key Functions +- Identify most important/used functions +- Explain inputs and outputs +- Note special requirements`} + +## Function Analysis Format + +For important functions provide: +- **Name**: Function name +- **Type**: View/Pure/Payable/Nonpayable +- **Inputs**: Parameter names and types with descriptions +- **Outputs**: Return values and types +- **Access**: Public/External/Restricted +- **Purpose**: What it does +- **Usage**: How to call it + +## Security Analysis + +Look for: +- **Proxy Patterns**: Is this a proxy contract? +- **Access Controls**: Who can call what? +- **Special Functions**: Initialization, upgrade paths +- **Obvious Issues**: Reentrancy risks, overflow/underflow patterns +- **Standard Compliance**: Is it ERC20/721/1155 compatible? + +## Output Format + +**Contract Analysis Report** + +- **Contract Type**: Identified purpose (Token/DEX/Lending/etc) +- **Network**: Where deployed +- **Verified**: Yes (since we fetched ABI) +- **Function Count**: Total functions by type + +**Function Categories**: +- View/Pure: [list of read functions] +- Write: [list of state-changing functions] +- Admin: [restricted functions] + +**Key Functions**: +[Detailed analysis of important functions] + +**Security Notes**: +[Vulnerabilities, patterns, recommendations] + +**How to Interact**: +[Step-by-step guide for common operations] +` } }] }) ); server.registerPrompt( - "explain_evm_concept", + "explore_contract", { - description: "Get an explanation of an EVM or blockchain concept", + description: "Analyze contract functions and state without requiring full ABI", argsSchema: { - concept: z.string().describe("The concept to explain (e.g., 'gas', 'nonce', 'smart contracts', 'MEV')") + contractAddress: z.string().describe("Contract address to explore"), + network: z.string().optional().describe("Network name (default: ethereum)"), + fetchAbi: z.string().optional().describe("Set to 'true' to auto-fetch ABI (requires ETHERSCAN_API_KEY)") } }, - ({ concept }) => ({ + ({ contractAddress, network = "ethereum", fetchAbi }) => ({ messages: [{ role: "user", content: { type: "text", - text: `Please explain the blockchain/EVM concept: "${concept}" + text: `# Contract Exploration + +**Objective**: Understand what contract ${contractAddress} does and how to use it + +## Exploration Strategy + +${fetchAbi === 'true' + ? `### With Full ABI (Fetched) +1. Call \`get_contract_abi\` to fetch verified ABI +2. Parse all available functions +3. Call \`read_contract\` for important state functions +4. Build comprehensive understanding +` + : `### Without Full ABI (Probing) +1. Test common function signatures +2. Call \`read_contract\` with standard functions: + - name(), symbol(), decimals(), totalSupply() + - owner(), paused(), version() + - balanceOf(), allowance(), totalSupply() +3. Infer contract type from successful calls +`} + +## Detection Process + +### 1. Identify Contract Type +Based on available functions, determine: +- **Token**: Has name, symbol, decimals, totalSupply, balanceOf +- **NFT/ERC721**: Has tokenURI, ownerOf, name, symbol +- **NFT/ERC1155**: Has uri, balanceOf, balanceOfBatch +- **Staking**: Has stake, unstake, reward, claim functions +- **DEX**: Has swap, liquidity, pair functions +- **Other**: Analyze unique functions + +### 2. Gather Key Information + +For each contract type: + +**Token (ERC20)**: +- name, symbol, decimals, totalSupply +- If owner, supply cap, minting rules +- If tax/fee mechanism + +**NFT (ERC721)**: +- name, symbol, totalSupply +- baseURI, tokenURI patterns +- royalty info if available + +**Staking/Farming**: +- Pool info, APY, reward token +- Lockup periods, early withdrawal penalties +- Reward distribution mechanism -In your explanation: -1. Define what it is in simple terms -2. Explain why it matters -3. Give practical examples -4. Explain how it affects using blockchain applications -5. If relevant, mention how to check or monitor it on the blockchain +### 3. Security Assessment +- Check for pause functions (risk of rug) +- Look for upgrade mechanisms (upgradeable proxy) +- Identify admin-only functions +- Note unusual patterns -Make it accessible for someone new to blockchain but interested in using EVM networks.` +## Output Format + +**Contract Overview** +- Address: [address] +- Type: [identified type] +- Network: [network] +- Verified: [yes/if ABI was fetched] + +**Key Properties** +[Type-specific details discovered] + +**Available Functions** +- Read-only: [list] +- State-changing: [list] +- Admin: [list if any] + +**How to Use** +[Step-by-step guide for primary use case] + +**Security Notes** +[Observations and recommendations] + +**Limitations** +[What couldn't be determined without full ABI] + +## When to Use ABI Fetch +- Need complete function list +- Want detailed parameter information +- Exploring unfamiliar/complex contracts +- Security due diligence +- Learn contract architecture +` } }] }) ); // ============================================================================ - // NETWORK INFORMATION PROMPTS + // NETWORK & EDUCATION PROMPTS // ============================================================================ server.registerPrompt( - "compare_networks", + "explain_evm_concept", { - description: "Compare different EVM networks to understand their differences", + description: "Explain EVM and blockchain concepts with examples", argsSchema: { - networks: z.string().describe("Comma-separated list of network names to compare (e.g., 'ethereum,polygon,arbitrum')") + concept: z.string().describe("Concept to explain (gas, nonce, smart contracts, MEV, etc)") } }, - ({ networks }) => { - const networkList = networks.split(',').map(n => n.trim()); - return { + ({ concept }) => ({ messages: [{ role: "user", content: { type: "text", - text: `Please compare these EVM networks: ${networkList.join(", ")} + text: `# Concept Explanation: ${concept} -For each network: -1. Call get_chain_info to get current chain ID and block number -2. Call get_gas_price to understand current gas costs -3. Provide comparison across: - - Chain characteristics (mainnet, testnet, etc.) - - Current gas prices - - Block time and finality - - Typical use cases and advantages - - Any known limitations or risks -4. Provide a recommendation for which network to use based on: - - Transaction speed needs - - Cost considerations - - Ecosystem size and liquidity - - Security considerations - -Make the comparison easy to understand for someone deciding which network to use.` +**Objective**: Provide clear, practical explanation of "${concept}" + +## Explanation Structure + +### 1. Definition +- What is it? +- Simple one-sentence summary +- Technical name/terminology + +### 2. How It Works +- Step-by-step explanation +- Why it exists/why it's important +- How it relates to blockchain + +### 3. Real-World Analogy +- Compare to familiar concept +- Make it relatable for beginners +- Highlight key differences + +### 4. Practical Examples +- Real transaction examples +- Numbers and metrics where applicable +- Common scenarios +- Edge cases or gotchas + +### 5. Relevance to Users +- Why should developers care? +- How does it affect transactions? +- How to optimize/reduce costs? +- Common mistakes to avoid + +## Output Format + +Provide explanation in sections: + +**What is ${concept}?** +[Definition and overview] + +**How Does It Work?** +[Mechanics and process] + +**Example** +[Real or hypothetical scenario] + +**Key Takeaways** +[Bullet points of important facts] + +**Common Questions** +- Question 1? Answer +- Question 2? Answer + +## Important +- Use clear, non-technical language first +- Progress to technical details +- Include concrete numbers where helpful +- Be honest about complexity +- Suggest further learning if needed +` } }] + }) + ); + + server.registerPrompt( + "compare_networks", + { + description: "Compare multiple EVM networks on key metrics and characteristics", + argsSchema: { + networks: z.string().describe("Comma-separated network names (ethereum,polygon,arbitrum)") + } + }, + ({ networks }) => { + const networkList = networks.split(',').map(n => n.trim()); + return { + messages: [{ + role: "user", + content: { + type: "text", + text: `# Network Comparison + +**Objective**: Compare ${networkList.join(', ')} on key metrics + +## Comparison Metrics + +### 1. Network Health (Current) +For each network, call: +- \`get_chain_info\` for chain ID and current block +- \`get_gas_price\` for current gas costs +- \`get_latest_block\` for block time and recent activity + +### 2. Key Characteristics +Compare across these dimensions: + +**Architecture**: +- Execution layer (Rollup/Sidechain/L1) +- Consensus mechanism +- Finality +- Decentralization level + +**Performance**: +- Block time (seconds per block) +- Transactions per second (TPS) +- Confirmation time +- Throughput + +**Costs**: +- Current gas prices (in gwei) +- Average transaction cost +- Cost to deploy contract +- Price trends + +**Security**: +- Validator count / decentralization +- Mainnet maturity +- Track record +- Security audits + +**Ecosystem**: +- Major protocols deployed +- Liquidity depth +- Developer activity +- Community size + +## Comparison Table + +Create table with: +- Network name +- Block time +- TPS capacity +- Current gas (gwei) +- Est. tx cost (USD) +- Security level +- Best for + +## Analysis + +For each network: +- **Strengths**: What it does well +- **Weaknesses**: Limitations +- **Best Use Cases**: When to use +- **Trade-offs**: Speed vs cost vs security + +## Recommendations + +Provide guidance: +- For small frequent transactions: [network] +- For large one-time transfers: [network] +- For DeFi/trading: [network] +- For NFTs: [network] +- For cost optimization: [network] + +## Output Format + +**Network Comparison Analysis** + +[Comparison table] + +**Network Profiles** + +For each network: +- Overview +- Current metrics +- Strengths +- Weaknesses +- Best use cases + +**Recommendations** + +Based on user needs: +- Speed priority: [suggestion] +- Cost priority: [suggestion] +- Security priority: [suggestion] +- Overall best: [suggestion] + +**Decision Matrix** + +Help user choose based on: +- Transaction frequency +- Transaction size +- Budget constraints +- Required finality +- Ecosystem needs +` + } + }] }; } ); @@ -284,9 +774,9 @@ Make the comparison easy to understand for someone deciding which network to use server.registerPrompt( "check_network_status", { - description: "Check the current status and health of an EVM network", + description: "Check current network health and conditions", argsSchema: { - network: z.string().optional().describe("Network name. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (default: ethereum)") } }, ({ network = "ethereum" }) => ({ @@ -294,19 +784,98 @@ Make the comparison easy to understand for someone deciding which network to use role: "user", content: { type: "text", - text: `Please check the current status of the ${network} network: - -1. Call get_chain_info to get chain ID, current block number, and RPC info -2. Call get_latest_block to see recent block information -3. Call get_gas_price to check current gas prices -4. Provide a status report with: - - Network is healthy/operational - - Current block number and recent block times - - Current gas prices (base fee and priority fee) - - Any observations about network congestion - - Recommendations for transaction timing if applicable - -Be clear about whether now is a good time to transact on this network.` + text: `# Network Status Check + +**Objective**: Assess health and current conditions of ${network} + +## Status Assessment + +### 1. Gather Current Data +Call these read-only tools: +- \`get_chain_info\` for chain ID and current block number +- \`get_latest_block\` for block details and timing +- \`get_gas_price\` for current gas prices + +### 2. Network Health Analysis + +**Block Production**: +- Current block number +- Block timing (normal ~12-15 sec for Ethereum) +- Consistent vs irregular blocks +- Any gaps or delays + +**Gas Market**: +- Base fee level (in gwei) +- Priority fee level +- Gas price trend (up/down/stable) +- Congestion level + +**Overall Status**: +- Operational: Yes/No +- Issues detected: Yes/No +- Performance: Normal/Degraded/Critical + +### 3. Congestion Assessment + +Evaluate: +- Current gas prices vs average +- Pending transaction count +- Memory pool size +- Are transactions backing up? + +## Output Format + +**Network Status Report: ${network}** + +**Overall Status** +- Operational Status: [Online/Degraded/Offline] +- Current Block: [number] +- Network Time: [timestamp] +- Last Updated: [when] + +**Performance Metrics** +- Block Time: [seconds] (normal: 12-15s) +- Gas Base Fee: [gwei] +- Priority Fee: [gwei] +- Total Cost for Standard Tx: [estimate USD] + +**Congestion Level** +- Level: [Low/Moderate/High/Critical] +- Current vs Historical: [comparison] +- Trend: [increasing/stable/decreasing] + +**Network Activity** +- Blocks per minute: [rate] +- Recent block details: [hash, time, tx count] +- Network security: [indicators] + +**Recommendations** + +For **sending transactions now**: +- Best for: [low-value / high-value / time-critical] +- Gas setting: [standard / fast / extreme] +- Estimated cost: [range] +- Estimated wait time: [minutes] + +**If Congested**: +- Consider using: [alternative networks] +- Wait time: [estimated minutes] +- Cost to expedite: [gas increase needed] + +**If Issues Detected**: +- Known issues: [list if any] +- Expected duration: [if known] +- Recommended action: [wait / use alternate / etc] + +## Key Metrics + +Reference points for interpretation: +- Ethereum normal block: 12-15 seconds +- Polygon normal: 2 seconds +- Arbitrum normal: <1 second +- Normal gas: 20-50 gwei +- High congestion: 100+ gwei +` } }] }) From 8cee62f8ba8b2461153623473847c480624ebf93 Mon Sep 17 00:00:00 2001 From: Valentin Cart Date: Wed, 19 Nov 2025 13:36:03 -0500 Subject: [PATCH 07/11] feat: add `write_contract` tool and `interact_with_contract` prompt for safe contract interaction. --- README.md | 8 +- package.json | 2 +- src/core/prompts.ts | 185 ++++++++++++++++++++++++++++++++++++++++++-- src/core/tools.ts | 113 +++++++++++++++++++++++++++ 4 files changed, 298 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4a6e413..fe29a73 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![MCP](https://img.shields.io/badge/MCP-1.22.0+-blue) ![Viem](https://img.shields.io/badge/Viem-2.39.3+-green) -A comprehensive Model Context Protocol (MCP) server that provides blockchain services across 60+ EVM-compatible networks. This server enables AI agents to interact with Ethereum, Optimism, Arbitrum, Base, Polygon, and many other EVM chains with a unified interface through 21 tools and 9 AI-guided prompts. +A comprehensive Model Context Protocol (MCP) server that provides blockchain services across 60+ EVM-compatible networks. This server enables AI agents to interact with Ethereum, Optimism, Arbitrum, Base, Polygon, and many other EVM chains with a unified interface through 22 tools and 10 AI-guided prompts. ## πŸ“‹ Contents @@ -75,7 +75,7 @@ All services are exposed through a consistent interface of MCP tools, resources, ### Smart Contract Interactions - **Read contract state** through view/pure functions -- **Write services** with private key signing +- **Write to contracts** - Execute any state-changing function with automatic ABI fetching - **Contract verification** to distinguish from EOAs - **Event logs** retrieval and filtering - **Automatic ABI fetching** from Etherscan v2 API across all 60+ networks (no need to know ABIs in advance) @@ -93,6 +93,7 @@ All services are exposed through a consistent interface of MCP tools, resources, - **Transaction preparation** - Guidance for planning and executing transfers - **Wallet analysis** - Tools for analyzing wallet activity and holdings - **Smart contract exploration** - Interactive ABI fetching and contract analysis +- **Contract interaction** - Safe execution of write operations on smart contracts - **Network information** - Learning about EVM networks and comparisons - **Approval auditing** - Reviewing and managing token approvals - **Error diagnosis** - Troubleshooting transaction failures @@ -440,7 +441,7 @@ console.log(result); ### Tools -The server provides 21 focused MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.** +The server provides 22 focused MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.** #### Wallet Information @@ -487,6 +488,7 @@ The server provides 21 focused MCP tools for agents. **All tools that accept add |-----------|-------------|----------------| | `get_contract_abi` | Fetch contract ABI from block explorer (60+ networks) | `contractAddress` (address/ENS), `network` | | `read_contract` | Read smart contract state (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `abiJson` (optional), `network` | +| `write_contract` | Execute state-changing functions (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `value` (optional), `abiJson` (optional), `network` | #### Token Transfers diff --git a/package.json b/package.json index 743614b..bffee87 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "module": "src/index.ts", "type": "module", "version": "2.0.0", - "description": "MCP server for interacting with EVM-compatible blockchains - supports 21 tools and 9 prompts across 60+ networks", + "description": "MCP server for interacting with EVM-compatible blockchains - supports 22 tools and 10 prompts across 60+ networks", "bin": { "evm-mcp-server": "./bin/cli.js" }, diff --git a/src/core/prompts.ts b/src/core/prompts.ts index 63f4c70..ada7df8 100644 --- a/src/core/prompts.ts +++ b/src/core/prompts.ts @@ -46,8 +46,8 @@ Before executing any transfer: 1. **Wallet Verification**: Call \`get_wallet_address\` to confirm the sending wallet 2. **Balance Check**: ${tokenType === "native" - ? "- Call `get_balance` to verify native token balance" - : "- Call `get_token_balance` with tokenAddress=${tokenAddress} to verify balance"} + ? "- Call `get_balance` to verify native token balance" + : "- Call `get_token_balance` with tokenAddress=${tokenAddress} to verify balance"} 3. **Gas Analysis**: Call \`get_gas_price\` to assess current network costs ${tokenType === "erc20" ? `4. **Approval Check**: Call \`get_allowance\` to verify approval (if needed for protocols)` : ""} @@ -204,8 +204,8 @@ Provide structured diagnosis: ### 3. Token Balances ${tokenList.length > 0 - ? `- Call \`get_token_balance\` for each token:\n${tokenList.map(t => ` * ${t}`).join('\n')}` - : `- If specific tokens provided: call \`get_token_balance\` for each + ? `- Call \`get_token_balance\` for each token:\n${tokenList.map(t => ` * ${t}`).join('\n')}` + : `- If specific tokens provided: call \`get_token_balance\` for each - Include token symbol and decimals if available`} ## Output Format @@ -475,13 +475,13 @@ Look for: ## Exploration Strategy ${fetchAbi === 'true' - ? `### With Full ABI (Fetched) + ? `### With Full ABI (Fetched) 1. Call \`get_contract_abi\` to fetch verified ABI 2. Parse all available functions 3. Call \`read_contract\` for important state functions 4. Build comprehensive understanding ` - : `### Without Full ABI (Probing) + : `### Without Full ABI (Probing) 1. Test common function signatures 2. Call \`read_contract\` with standard functions: - name(), symbol(), decimals(), totalSupply() @@ -567,6 +567,179 @@ For each contract type: // NETWORK & EDUCATION PROMPTS // ============================================================================ + server.registerPrompt( + "interact_with_contract", + { + description: "Safely execute write operations on a smart contract with validation and confirmation", + argsSchema: { + contractAddress: z.string().describe("Contract address to interact with"), + functionName: z.string().describe("Function to call (e.g., 'mint', 'swap', 'stake')"), + args: z.string().optional().describe("Comma-separated function arguments"), + value: z.string().optional().describe("ETH value to send (for payable functions)"), + network: z.string().optional().describe("Network name (default: ethereum)") + } + }, + ({ contractAddress, functionName, args, value, network = "ethereum" }) => { + const argsList = args ? args.split(',').map(a => a.trim()) : []; + return { + messages: [{ + role: "user", + content: { + type: "text", + text: `# Smart Contract Interaction + +**Objective**: Safely execute ${functionName} on contract ${contractAddress} on ${network} + +## Prerequisites Check + +### 1. Wallet Verification +- Call \`get_wallet_address\` to confirm the wallet that will execute this transaction +- Verify this is the correct wallet for this operation + +### 2. Contract Analysis +- Call \`get_contract_abi\` to fetch and analyze the contract ABI +- Verify the function exists and understand its parameters +- Check function type: + * **View/Pure**: Read-only (use \`read_contract\` instead) + * **Nonpayable**: State-changing, no ETH required + * **Payable**: State-changing, can accept ETH + +### 3. Function Parameter Validation +For function: **${functionName}** +${argsList.length > 0 ? `Arguments provided: ${argsList.join(', ')}` : 'No arguments provided'} + +- Verify parameter types match the ABI +- Validate addresses are checksummed +- Check numeric values are in correct units +- Resolve any ENS names to addresses if needed + +### 4. Pre-execution Checks + +**Balance Check**: +- Call \`get_balance\` to verify sufficient native token balance +- Account for gas costs + value (if payable) + +**Gas Estimation**: +- Call \`get_gas_price\` to estimate transaction cost +- Calculate total cost: (gas_price * estimated_gas) + value + +**State Verification** (if applicable): +- Use \`read_contract\` to check current contract state +- Verify conditions are met (e.g., allowances, balances, ownership) + +## Execution Process + +### 1. Present Summary to User +Before executing, show: +- **Contract**: ${contractAddress} +- **Network**: ${network} +- **Function**: ${functionName} +- **Arguments**: ${argsList.length > 0 ? argsList.join(', ') : 'None'} +${value ? `- **Value**: ${value} ETH` : ''} +- **From**: [wallet address from step 1] +- **Estimated Gas Cost**: [from gas estimation] +- **Total Cost**: [gas + value] + +### 2. Request User Confirmation +⚠️ **IMPORTANT**: Always ask user to confirm before executing write operations +- Clearly state what will happen +- Show all costs involved +- Explain any risks or irreversible actions + +### 3. Execute Transaction +Only after user confirms: +\`\`\` +Call write_contract with: +- contractAddress: "${contractAddress}" +- functionName: "${functionName}" +${argsList.length > 0 ? `- args: ${JSON.stringify(argsList)}` : ''} +${value ? `- value: "${value}"` : ''} +- network: "${network}" +\`\`\` + +### 4. Monitor Transaction +After execution: +1. Return transaction hash to user +2. Call \`wait_for_transaction\` to monitor confirmation +3. Call \`get_transaction_receipt\` to verify success +4. If failed, call \`diagnose_transaction\` to understand why + +## Output Format + +**Pre-Execution Summary**: +- Contract details +- Function and parameters +- Cost breakdown +- Risk assessment + +**Confirmation Request**: +"Ready to execute ${functionName} on ${contractAddress}. This will cost approximately [X] ETH. Proceed? (yes/no)" + +**Execution Result**: +- Transaction Hash: [hash] +- Status: Pending/Confirmed/Failed +- Block Number: [if confirmed] +- Gas Used: [actual gas used] +- Total Cost: [final cost] + +## Safety Considerations + +### Critical Checks +- βœ… Verify contract is verified on block explorer +- βœ… Check function parameters are correct type and format +- βœ… Ensure sufficient balance for gas + value +- βœ… Validate addresses (no typos, correct network) +- βœ… Understand what the function does before calling + +### Common Risks +- **Irreversible**: Most blockchain transactions cannot be undone +- **Gas Loss**: Failed transactions still consume gas +- **Approval Risks**: Be careful with unlimited approvals +- **Reentrancy**: Some functions may be vulnerable +- **Access Control**: Verify you have permission to call this function + +### Red Flags +🚨 Stop and warn user if: +- Contract is not verified +- Function requires admin/owner privileges you don't have +- Unusually high gas estimate +- Suspicious parameter values +- Contract has known vulnerabilities + +## Error Handling + +If transaction fails: +1. Get the revert reason from receipt +2. Check common issues: + - Insufficient balance/allowance + - Access control (onlyOwner, etc.) + - Invalid parameters + - Contract paused + - Slippage (for DEX operations) +3. Provide actionable fix suggestions +4. Offer to retry with corrected parameters + +## Example Workflow + +For a token mint operation: +1. βœ… Verify wallet +2. βœ… Fetch contract ABI +3. βœ… Check mint function exists and is callable +4. βœ… Verify sufficient ETH for gas +5. βœ… Show summary: "Minting 1 NFT will cost ~0.002 ETH" +6. ⏸️ Wait for user confirmation +7. βœ… Execute write_contract +8. βœ… Monitor transaction +9. βœ… Confirm success and return token ID + +**Remember**: Always prioritize user safety and transparency! +` + } + }] + }; + } + ); + server.registerPrompt( "explain_evm_concept", { diff --git a/src/core/tools.ts b/src/core/tools.ts index de4a269..9c3dd33 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -765,6 +765,119 @@ export function registerEVMTools(server: McpServer) { } ); + server.registerTool( + "write_contract", + { + description: "Execute state-changing functions on a smart contract. Automatically fetches ABI from block explorer if not provided (requires ETHERSCAN_API_KEY). Use this to call any write function on verified contracts. Requires EVM_PRIVATE_KEY to be configured.", + inputSchema: { + contractAddress: z.string().describe("The contract address"), + functionName: z.string().describe("Function name to call (e.g., 'mint', 'swap', 'stake', 'approve')"), + args: z.array(z.string()).optional().describe("Function arguments as strings (e.g., ['0xAddress', '1000000'])"), + value: z.string().optional().describe("ETH value to send with transaction in ether (e.g., '0.1' for payable functions)"), + abiJson: z.string().optional().describe("Full contract ABI as JSON string (optional - will auto-fetch verified contract ABI if not provided)"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Write to Smart Contract", + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true + } + }, + async ({ contractAddress, functionName, args = [], value, abiJson, network = "ethereum" }) => { + try { + const privateKey = getConfiguredPrivateKey(); + const senderAddress = getWalletAddressFromKey(); + const client = await services.getPublicClient(network); + + let abi: any[] | undefined; + let functionAbi: any; + + // If ABI is provided, use it + if (abiJson) { + try { + abi = services.parseABI(abiJson); + functionAbi = services.getFunctionFromABI(abi, functionName); + } catch (error) { + return { + content: [{ + type: "text", + text: `Error parsing provided ABI: ${error instanceof Error ? error.message : String(error)}` + }], + isError: true + }; + } + } else { + // Try to auto-fetch ABI from block explorer + try { + const fetchedAbi = await services.fetchContractABI(contractAddress as Address, network); + abi = services.parseABI(fetchedAbi); + functionAbi = services.getFunctionFromABI(abi, functionName); + } catch (fetchError) { + return { + content: [{ + type: "text", + text: `Error: Could not auto-fetch ABI (${fetchError instanceof Error ? fetchError.message : String(fetchError)}). Please provide the contract ABI using the abiJson parameter, or use get_contract_abi to fetch it first.` + }], + isError: true + }; + } + } + + // Validate that this is not a view/pure function + if (functionAbi.stateMutability === 'view' || functionAbi.stateMutability === 'pure') { + return { + content: [{ + type: "text", + text: `Error: Function '${functionName}' is a ${functionAbi.stateMutability} function and cannot modify state. Use read_contract instead.` + }], + isError: true + }; + } + + // Prepare write parameters + const writeParams: any = { + address: contractAddress as Address, + abi: [functionAbi], + functionName: functionName, + args: args as any + }; + + // Add value if provided (for payable functions) + if (value) { + const { parseEther } = await import('viem'); + writeParams.value = parseEther(value); + } + + // Execute the write operation + const txHash = await services.writeContract(privateKey, writeParams, network); + + return { + content: [{ + type: "text", + text: JSON.stringify({ + network, + contractAddress, + function: functionName, + args: args.length > 0 ? args : undefined, + value: value || undefined, + from: senderAddress, + txHash, + abiSource: abiJson ? 'provided' : 'auto-fetched', + message: "Transaction sent. Use get_transaction_receipt or wait_for_transaction to check confirmation." + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Error writing to contract: ${error instanceof Error ? error.message : String(error)}` }], + isError: true + }; + } + } + ); + // ============================================================================ // TRANSFER TOOLS (Write operations) // ============================================================================ From 9e2a0ac103ad20e42ac14c405ab8c7acad8e78d7 Mon Sep 17 00:00:00 2001 From: Valentin Cart Date: Wed, 19 Nov 2025 14:03:18 -0500 Subject: [PATCH 08/11] feat: Add flexible wallet configuration supporting private key or mnemonic phrase, centralize wallet logic, and update documentation. --- README.md | 29 +++++++++++++++-- src/core/services/index.ts | 7 +++-- src/core/services/wallet.ts | 62 +++++++++++++++++++++++++++++++++++++ src/core/tools.ts | 35 ++++++--------------- 4 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 src/core/services/wallet.ts diff --git a/README.md b/README.md index fe29a73..322b31a 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ All services are exposed through a consistent interface of MCP tools, resources, ### Comprehensive Transaction Support +- **Flexible Wallet Support** - Configure with Private Key or Mnemonic (BIP-39) with HD path support - **Native token transfers** across all supported networks - **Gas estimation** for transaction planning - **Transaction status** and receipt information @@ -185,18 +186,40 @@ npm install The server uses the following environment variables. For write operations and ABI fetching, you must configure these variables: -#### Private Key (For Write Operations) +#### Wallet Configuration (For Write Operations) + +You can configure your wallet using **either** a private key or a mnemonic phrase: + +**Option 1: Private Key** ```bash export EVM_PRIVATE_KEY="0x..." # Your private key in hex format (with or without 0x prefix) ``` -This private key is used for: +**Option 2: Mnemonic Phrase (Recommended for HD Wallets)** + +```bash +export EVM_MNEMONIC="word1 word2 word3 ... word12" # Your 12 or 24 word BIP-39 mnemonic +export EVM_ACCOUNT_INDEX="0" # Optional: Account index for HD wallet derivation (default: 0) +``` + +The mnemonic option supports hierarchical deterministic (HD) wallet derivation: +- Uses BIP-39 standard mnemonic phrases (12 or 24 words) +- Supports BIP-44 derivation path: `m/44'/60'/0'/0/{accountIndex}` +- `EVM_ACCOUNT_INDEX` allows you to derive different accounts from the same mnemonic +- Default account index is 0 (first account) + +**Wallet is used for:** - Transferring native tokens (`transfer_native` tool) - Transferring ERC20 tokens (`transfer_erc20` tool) - Approving token spending (`approve_token_spending` tool) +- Writing to smart contracts (`write_contract` tool) -⚠️ **Security**: Never commit your private key to version control. Use environment variables or a secure key management system. +⚠️ **Security**: +- Never commit your private key or mnemonic to version control +- Use environment variables or a secure key management system +- Store mnemonics securely - they provide access to all derived accounts +- Consider using different account indices for different purposes #### API Keys (For ABI Fetching) diff --git a/src/core/services/index.ts b/src/core/services/index.ts index 27345b7..14108ed 100644 --- a/src/core/services/index.ts +++ b/src/core/services/index.ts @@ -8,12 +8,13 @@ export * from './contracts.js'; export * from './tokens.js'; export * from './ens.js'; export * from './abi.js'; +export * from './wallet.js'; export { utils as helpers } from './utils.js'; // Re-export common types for convenience -export type { - Address, - Hash, +export type { + Address, + Hash, Hex, Block, TransactionReceipt, diff --git a/src/core/services/wallet.ts b/src/core/services/wallet.ts new file mode 100644 index 0000000..061fa66 --- /dev/null +++ b/src/core/services/wallet.ts @@ -0,0 +1,62 @@ +import { type Address, type Hex } from 'viem'; +import { privateKeyToAccount, mnemonicToAccount } from 'viem/accounts'; + +/** + * Get the configured account from environment (private key or mnemonic) + * + * Configuration options: + * - EVM_PRIVATE_KEY: Hex private key (with or without 0x prefix) + * - EVM_MNEMONIC: BIP-39 mnemonic phrase (12 or 24 words) + * - EVM_ACCOUNT_INDEX: Optional account index for HD wallet derivation (default: 0) + */ +export const getConfiguredAccount = () => { + const privateKey = process.env.EVM_PRIVATE_KEY; + const mnemonic = process.env.EVM_MNEMONIC; + const accountIndex = parseInt(process.env.EVM_ACCOUNT_INDEX || '0'); + + if (privateKey) { + // Use private key if provided + const key = (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`) as Hex; + return privateKeyToAccount(key); + } else if (mnemonic) { + // Use mnemonic if provided + return mnemonicToAccount(mnemonic, { accountIndex }); + } else { + throw new Error( + "Neither EVM_PRIVATE_KEY nor EVM_MNEMONIC environment variable is set. " + + "Configure one of them to enable write operations.\n" + + "- EVM_PRIVATE_KEY: Your private key in hex format\n" + + "- EVM_MNEMONIC: Your 12 or 24 word mnemonic phrase\n" + + "- EVM_ACCOUNT_INDEX: (Optional) Account index for HD wallet (default: 0)" + ); + } +}; + +/** + * Helper to get the configured private key (for services that need it) + */ +export const getConfiguredPrivateKey = (): Hex => { + const account = getConfiguredAccount(); + // For mnemonic-based accounts, we need to extract the private key + // Viem accounts have a privateKey property but it's not always in the type definition + const accountWithKey = account as any; + if (!accountWithKey.privateKey) { + throw new Error("Unable to extract private key from account"); + } + return accountWithKey.privateKey; +}; + +/** + * Helper to get wallet address + */ +export const getWalletAddressFromKey = (): Address => { + const account = getConfiguredAccount(); + return account.address; +}; + +/** + * Helper to get configured wallet object + */ +export const getConfiguredWallet = (): { address: Address } => { + return { address: getWalletAddressFromKey() }; +}; diff --git a/src/core/tools.ts b/src/core/tools.ts index 9c3dd33..ee2764b 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -3,43 +3,28 @@ import { z } from "zod"; import { getSupportedNetworks, getRpcUrl } from "./chains.js"; import * as services from "./services/index.js"; import { type Address, type Hex, type Hash } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; import { normalize } from 'viem/ens'; /** * Register all EVM-related tools with the MCP server * - * SECURITY: The EVM_PRIVATE_KEY environment variable must be set for write operations. - * Private keys are never passed as tool arguments for security reasons. + * SECURITY: Either EVM_PRIVATE_KEY or EVM_MNEMONIC environment variable must be set for write operations. + * Private keys and mnemonics are never passed as tool arguments for security reasons. * Tools will use the configured wallet for all transactions. * + * Configuration options: + * - EVM_PRIVATE_KEY: Hex private key (with or without 0x prefix) + * - EVM_MNEMONIC: BIP-39 mnemonic phrase (12 or 24 words) + * - EVM_ACCOUNT_INDEX: Optional account index for HD wallet derivation (default: 0) + * * All tools that accept addresses also support ENS names (e.g., 'vitalik.eth'). * ENS names are automatically resolved to addresses using the Ethereum Name Service. * * @param server The MCP server instance */ export function registerEVMTools(server: McpServer) { - // Helper to get the configured private key from environment - const getConfiguredPrivateKey = (): Hex => { - const privateKey = process.env.EVM_PRIVATE_KEY; - if (!privateKey) { - throw new Error("EVM_PRIVATE_KEY environment variable is not set. Configure it to enable write operations."); - } - // Ensure 0x prefix - return (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`) as Hex; - }; - - // Helper to get wallet address from private key - const getWalletAddressFromKey = (): Address => { - const privateKey = getConfiguredPrivateKey(); - const account = privateKeyToAccount(privateKey); - return account.address; - }; - - // Helper to get configured wallet object - const getConfiguredWallet = (): { address: Address } => { - return { address: getWalletAddressFromKey() }; - }; + // Helpers are now imported from services/wallet.ts + const { getConfiguredPrivateKey, getWalletAddressFromKey, getConfiguredWallet } = services; // ============================================================================ // WALLET INFORMATION TOOLS (Read-only) @@ -768,7 +753,7 @@ export function registerEVMTools(server: McpServer) { server.registerTool( "write_contract", { - description: "Execute state-changing functions on a smart contract. Automatically fetches ABI from block explorer if not provided (requires ETHERSCAN_API_KEY). Use this to call any write function on verified contracts. Requires EVM_PRIVATE_KEY to be configured.", + description: "Execute state-changing functions on a smart contract. Automatically fetches ABI from block explorer if not provided (requires ETHERSCAN_API_KEY). Use this to call any write function on verified contracts. Requires wallet to be configured (via private key or mnemonic).", inputSchema: { contractAddress: z.string().describe("The contract address"), functionName: z.string().describe("Function name to call (e.g., 'mint', 'swap', 'stake', 'approve')"), From 800407ac9a5c64572ad56a253c3287fcec8197df Mon Sep 17 00:00:00 2001 From: Valentin Cart Date: Wed, 19 Nov 2025 14:19:22 -0500 Subject: [PATCH 09/11] feat!: Add PR description for v2.0.0 release and update package version to `2.0.0-pre`. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bffee87..475a119 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@mcpdotdirect/evm-mcp-server", "module": "src/index.ts", "type": "module", - "version": "2.0.0", + "version": "2.0.0-pre", "description": "MCP server for interacting with EVM-compatible blockchains - supports 22 tools and 10 prompts across 60+ networks", "bin": { "evm-mcp-server": "./bin/cli.js" From 07687b1f2f74ad05b4919e0d1342957665943188 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Nov 2025 14:50:01 +0000 Subject: [PATCH 10/11] fix: add Express back for reliability and improve wallet.ts type safety - Add express and @types/express to dependencies for production reliability * Battle-tested error handling and middleware ecosystem * All MCP SDK examples use Express for HTTP transport * Minimal overhead (~50KB) with significant reliability gains - Fix wallet.ts to use viem's documented HDAccount/PrivateKeyAccount types * Use getHdKey() method for extracting private key from mnemonic accounts * Proper type checking instead of unsafe 'as any' casting * Better error messages for debugging - Update bun.lock with express dependencies This ensures all MCP transports (stdio, StreamableHTTP) are production-ready. --- bun.lock | 125 ++++++++++++++++++++++++++++-------- package.json | 2 + src/core/services/wallet.ts | 35 +++++++--- 3 files changed, 128 insertions(+), 34 deletions(-) diff --git a/bun.lock b/bun.lock index d0e577c..1179d0d 100644 --- a/bun.lock +++ b/bun.lock @@ -1,15 +1,18 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "mcp-evm-server", "dependencies": { "@modelcontextprotocol/sdk": "^1.22.0", + "express": "^4.21.2", "viem": "^2.39.3", "zod": "^3.24.3", }, "devDependencies": { "@types/bun": "latest", + "@types/express": "^5.0.0", "@types/node": "^22.0.0", "conventional-changelog-cli": "^5.0.0", }, @@ -43,19 +46,39 @@ "@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + "@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="], + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/express": ["@types/express@5.0.5", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^1" } }, "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], + "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + + "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], "abitype": ["abitype@1.1.0", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A=="], - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "add-stream": ["add-stream@1.0.0", "", {}, "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ=="], @@ -63,9 +86,11 @@ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], - "body-parser": ["body-parser@2.1.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.5.2", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ=="], + "body-parser": ["body-parser@1.20.3", "", { "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.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], @@ -77,7 +102,7 @@ "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], - "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -115,13 +140,13 @@ "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "debug": ["debug@4.3.6", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg=="], + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], @@ -151,7 +176,7 @@ "eventsource-parser": ["eventsource-parser@3.0.0", "", {}, "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA=="], - "express": ["express@5.0.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.0.1", "content-disposition": "^1.0.0", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "^1.2.1", "debug": "4.3.6", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "^2.0.0", "fresh": "2.0.0", "http-errors": "2.0.0", "merge-descriptors": "^2.0.0", "methods": "~1.1.2", "mime-types": "^3.0.0", "on-finished": "2.4.1", "once": "1.4.0", "parseurl": "~1.3.3", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "router": "^2.0.0", "safe-buffer": "5.2.1", "send": "^1.1.0", "serve-static": "^2.1.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "^2.0.0", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ=="], + "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "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.13.0", "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" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], "express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], @@ -159,13 +184,13 @@ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + "finalhandler": ["finalhandler@1.3.1", "", { "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.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], "find-up-simple": ["find-up-simple@1.0.1", "", {}, "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -213,23 +238,25 @@ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], "meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="], - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "mime-db": ["mime-db@1.53.0", "", {}, "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg=="], "mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], @@ -251,7 +278,7 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -279,9 +306,9 @@ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "send": ["send@1.1.0", "", { "dependencies": { "debug": "^4.3.5", "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.0", "mime-types": "^2.1.35", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA=="], + "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], - "serve-static": ["serve-static@2.1.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.0.0" } }, "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA=="], + "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], @@ -317,7 +344,7 @@ "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "type-is": ["type-is@2.0.0", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw=="], + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], @@ -349,24 +376,72 @@ "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], - "body-parser/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@modelcontextprotocol/sdk/express": ["express@5.0.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.0.1", "content-disposition": "^1.0.0", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "^1.2.1", "debug": "4.3.6", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "^2.0.0", "fresh": "2.0.0", "http-errors": "2.0.0", "merge-descriptors": "^2.0.0", "methods": "~1.1.2", "mime-types": "^3.0.0", "on-finished": "2.4.1", "once": "1.4.0", "parseurl": "~1.3.3", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "router": "^2.0.0", "safe-buffer": "5.2.1", "send": "^1.1.0", "serve-static": "^2.1.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "^2.0.0", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ=="], - "body-parser/iconv-lite": ["iconv-lite@0.5.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "finalhandler/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "body-parser/raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], - "send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], - "send/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "body-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.1.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.5.2", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ=="], + + "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], + + "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "@modelcontextprotocol/sdk/express/debug": ["debug@4.3.6", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg=="], + + "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + + "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "@modelcontextprotocol/sdk/express/send": ["send@1.1.0", "", { "dependencies": { "debug": "^4.3.5", "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.0", "mime-types": "^2.1.35", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA=="], + + "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.1.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.0.0" } }, "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA=="], + + "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.0", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw=="], + + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "@modelcontextprotocol/sdk/express/body-parser/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.5.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag=="], + + "@modelcontextprotocol/sdk/express/body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "@modelcontextprotocol/sdk/express/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "@modelcontextprotocol/sdk/express/finalhandler/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@modelcontextprotocol/sdk/express/send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "@modelcontextprotocol/sdk/express/send/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "@modelcontextprotocol/sdk/express/send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "@modelcontextprotocol/sdk/express/body-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "finalhandler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@modelcontextprotocol/sdk/express/finalhandler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "send/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "@modelcontextprotocol/sdk/express/send/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], } } diff --git a/package.json b/package.json index 475a119..c753a8d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/express": "^5.0.0", "@types/node": "^22.0.0", "conventional-changelog-cli": "^5.0.0" }, @@ -40,6 +41,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.22.0", + "express": "^4.21.2", "viem": "^2.39.3", "zod": "^3.24.3" }, diff --git a/src/core/services/wallet.ts b/src/core/services/wallet.ts index 061fa66..4bbe224 100644 --- a/src/core/services/wallet.ts +++ b/src/core/services/wallet.ts @@ -1,15 +1,15 @@ import { type Address, type Hex } from 'viem'; -import { privateKeyToAccount, mnemonicToAccount } from 'viem/accounts'; +import { privateKeyToAccount, mnemonicToAccount, type HDAccount, type PrivateKeyAccount } from 'viem/accounts'; /** * Get the configured account from environment (private key or mnemonic) - * + * * Configuration options: * - EVM_PRIVATE_KEY: Hex private key (with or without 0x prefix) * - EVM_MNEMONIC: BIP-39 mnemonic phrase (12 or 24 words) * - EVM_ACCOUNT_INDEX: Optional account index for HD wallet derivation (default: 0) */ -export const getConfiguredAccount = () => { +export const getConfiguredAccount = (): HDAccount | PrivateKeyAccount => { const privateKey = process.env.EVM_PRIVATE_KEY; const mnemonic = process.env.EVM_MNEMONIC; const accountIndex = parseInt(process.env.EVM_ACCOUNT_INDEX || '0'); @@ -34,16 +34,33 @@ export const getConfiguredAccount = () => { /** * Helper to get the configured private key (for services that need it) + * + * For HDAccount (from mnemonic): extracts private key from HD key + * For PrivateKeyAccount: returns the original private key */ export const getConfiguredPrivateKey = (): Hex => { const account = getConfiguredAccount(); - // For mnemonic-based accounts, we need to extract the private key - // Viem accounts have a privateKey property but it's not always in the type definition - const accountWithKey = account as any; - if (!accountWithKey.privateKey) { - throw new Error("Unable to extract private key from account"); + + // Check if this is an HDAccount (has getHdKey method) + if ('getHdKey' in account && typeof account.getHdKey === 'function') { + const hdKey = account.getHdKey(); + if (!hdKey.privateKey) { + throw new Error("Unable to derive private key from HD account - no private key in HD key"); + } + // Convert Uint8Array to hex string + const privateKeyHex = Buffer.from(hdKey.privateKey).toString('hex'); + return `0x${privateKeyHex}` as Hex; + } + + // For PrivateKeyAccount, re-read from environment since we created from it + if ('source' in account && account.source === 'privateKey') { + const privateKey = process.env.EVM_PRIVATE_KEY; + if (privateKey) { + return (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`) as Hex; + } } - return accountWithKey.privateKey; + + throw new Error("Unable to extract private key from account"); }; /** From b02dad95a15628debc8ec129aee297d951f36fc0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Nov 2025 14:53:32 +0000 Subject: [PATCH 11/11] fix: add production hardening for security and reliability Security Fixes: - Add 10mb JSON body size limit to prevent DoS attacks - Validate EVM_ACCOUNT_INDEX to prevent NaN injection - Add HTTP server timeouts (120s request, 65s keepalive) Reliability Fixes: - Implement session cleanup (30min timeout, check every 5min) - Update session timestamps on activity to prevent premature cleanup - Prevent memory leaks from abandoned sessions These changes ensure the server is production-ready and resilient. --- src/core/services/wallet.ts | 10 +++++++++- src/server/http-server.ts | 31 +++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/core/services/wallet.ts b/src/core/services/wallet.ts index 4bbe224..799bc10 100644 --- a/src/core/services/wallet.ts +++ b/src/core/services/wallet.ts @@ -12,7 +12,15 @@ import { privateKeyToAccount, mnemonicToAccount, type HDAccount, type PrivateKey export const getConfiguredAccount = (): HDAccount | PrivateKeyAccount => { const privateKey = process.env.EVM_PRIVATE_KEY; const mnemonic = process.env.EVM_MNEMONIC; - const accountIndex = parseInt(process.env.EVM_ACCOUNT_INDEX || '0'); + const accountIndexStr = process.env.EVM_ACCOUNT_INDEX || '0'; + const accountIndex = parseInt(accountIndexStr, 10); + + // Validate account index + if (isNaN(accountIndex) || accountIndex < 0 || !Number.isInteger(accountIndex)) { + throw new Error( + `Invalid EVM_ACCOUNT_INDEX: "${accountIndexStr}". Must be a non-negative integer.` + ); + } if (privateKey) { // Use private key if provided diff --git a/src/server/http-server.ts b/src/server/http-server.ts index 5774fc9..cb20194 100644 --- a/src/server/http-server.ts +++ b/src/server/http-server.ts @@ -12,10 +12,30 @@ console.error(`Configured to listen on ${HOST}:${PORT}`); // Setup Express const app = express(); -app.use(express.json()); +app.use(express.json({ limit: '10mb' })); // Prevent DoS attacks with huge payloads -// Track active transports by session ID +// Track active transports by session ID with cleanup const transports = new Map(); +const sessionTimestamps = new Map(); +const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes + +// Cleanup stale sessions periodically +setInterval(() => { + const now = Date.now(); + for (const [sessionId, timestamp] of sessionTimestamps.entries()) { + if (now - timestamp > SESSION_TIMEOUT_MS) { + console.error(`Cleaning up stale session: ${sessionId}`); + const transport = transports.get(sessionId); + if (transport) { + transport.close().catch(err => + console.error(`Error closing stale session ${sessionId}:`, err) + ); + } + transports.delete(sessionId); + sessionTimestamps.delete(sessionId); + } + } +}, 5 * 60 * 1000); // Check every 5 minutes // Initialize the MCP server let server: McpServer | null = null; @@ -44,6 +64,7 @@ app.post("/mcp", async (req: Request, res: Response) => { if (sessionId && transports.has(sessionId)) { // Reuse existing transport for this session transport = transports.get(sessionId)!; + sessionTimestamps.set(sessionId, Date.now()); // Update last activity console.error(`Reusing transport for session: ${sessionId}`); } else if (!sessionId) { // New session - create transport with session ID generator @@ -52,10 +73,12 @@ app.post("/mcp", async (req: Request, res: Response) => { onsessioninitialized: (newSessionId) => { console.error(`Session initialized: ${newSessionId}`); transports.set(newSessionId, transport); + sessionTimestamps.set(newSessionId, Date.now()); }, onsessionclosed: (closedSessionId) => { console.error(`Session closed: ${closedSessionId}`); transports.delete(closedSessionId); + sessionTimestamps.delete(closedSessionId); } }); @@ -191,3 +214,7 @@ const httpServer = app.listen(PORT, HOST, () => { console.error(`Server error: ${err}`); process.exit(1); }); + +// Set server timeout to prevent hanging connections +httpServer.timeout = 120000; // 2 minutes +httpServer.keepAliveTimeout = 65000; // 65 seconds