web3-tools-mcp 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -230
- package/dist/package.json +63 -0
- package/dist/src/client.d.ts +1653 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +142 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +88 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/tools/advanced.d.ts +75 -0
- package/dist/src/tools/advanced.d.ts.map +1 -0
- package/dist/src/tools/advanced.js +234 -0
- package/dist/src/tools/advanced.js.map +1 -0
- package/dist/src/tools/balance.d.ts +55 -0
- package/dist/src/tools/balance.d.ts.map +1 -0
- package/dist/src/tools/balance.js +146 -0
- package/dist/src/tools/balance.js.map +1 -0
- package/dist/src/tools/contract-info.d.ts +79 -0
- package/dist/src/tools/contract-info.d.ts.map +1 -0
- package/dist/src/tools/contract-info.js +450 -0
- package/dist/src/tools/contract-info.js.map +1 -0
- package/dist/src/tools/contract.d.ts +79 -0
- package/dist/src/tools/contract.d.ts.map +1 -0
- package/dist/src/tools/contract.js +140 -0
- package/dist/src/tools/contract.js.map +1 -0
- package/dist/src/tools/ens.d.ts +119 -0
- package/dist/src/tools/ens.d.ts.map +1 -0
- package/dist/src/tools/ens.js +425 -0
- package/dist/src/tools/ens.js.map +1 -0
- package/dist/src/tools/index.d.ts +3 -0
- package/dist/src/tools/index.d.ts.map +1 -0
- package/dist/src/tools/index.js +29 -0
- package/dist/src/tools/index.js.map +1 -0
- package/dist/src/tools/logs.d.ts +39 -0
- package/dist/src/tools/logs.d.ts.map +1 -0
- package/dist/src/tools/logs.js +231 -0
- package/dist/src/tools/logs.js.map +1 -0
- package/dist/src/tools/signatures.d.ts +83 -0
- package/dist/src/tools/signatures.d.ts.map +1 -0
- package/dist/src/tools/signatures.js +93 -0
- package/dist/src/tools/signatures.js.map +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils.d.ts +22 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +183 -0
- package/dist/src/utils.js.map +1 -0
- package/dist/test/tools/advanced.test.d.ts +2 -0
- package/dist/test/tools/advanced.test.d.ts.map +1 -0
- package/dist/test/tools/advanced.test.js +235 -0
- package/dist/test/tools/advanced.test.js.map +1 -0
- package/dist/test/tools/balance.test.d.ts +2 -0
- package/dist/test/tools/balance.test.d.ts.map +1 -0
- package/dist/test/tools/balance.test.js +167 -0
- package/dist/test/tools/balance.test.js.map +1 -0
- package/dist/test/tools/contract-info.test.d.ts +2 -0
- package/dist/test/tools/contract-info.test.d.ts.map +1 -0
- package/dist/test/tools/contract-info.test.js +286 -0
- package/dist/test/tools/contract-info.test.js.map +1 -0
- package/dist/test/tools/contract.test.d.ts +2 -0
- package/dist/test/tools/contract.test.d.ts.map +1 -0
- package/dist/test/tools/contract.test.js +234 -0
- package/dist/test/tools/contract.test.js.map +1 -0
- package/dist/test/tools/ens.test.d.ts +2 -0
- package/dist/test/tools/ens.test.d.ts.map +1 -0
- package/dist/test/tools/ens.test.js +236 -0
- package/dist/test/tools/ens.test.js.map +1 -0
- package/dist/test/tools/logs.test.d.ts +2 -0
- package/dist/test/tools/logs.test.d.ts.map +1 -0
- package/dist/test/tools/logs.test.js +196 -0
- package/dist/test/tools/logs.test.js.map +1 -0
- package/dist/test/tools/signatures.test.d.ts +2 -0
- package/dist/test/tools/signatures.test.d.ts.map +1 -0
- package/dist/test/tools/signatures.test.js +112 -0
- package/dist/test/tools/signatures.test.js.map +1 -0
- package/package.json +37 -22
- package/src/client.ts +160 -0
- package/src/index.ts +96 -0
- package/src/tools/advanced.ts +262 -0
- package/src/tools/balance.ts +176 -0
- package/src/tools/contract-info.ts +582 -0
- package/src/tools/contract.ts +180 -0
- package/src/tools/ens.ts +493 -0
- package/src/tools/index.ts +37 -0
- package/src/tools/logs.ts +291 -0
- package/src/tools/signatures.ts +126 -0
- package/src/types.ts +29 -0
- package/src/utils.ts +222 -0
- package/dist/hypersync-client.darwin-arm64-7fegfwn5.node +0 -0
- package/dist/index.js +0 -28282
package/package.json
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "web3-tools-mcp",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Model Context Protocol server for blockchain interactions using viem, Etherscan, and Hypersync",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
|
-
"
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
7
8
|
"bin": {
|
|
8
9
|
"web3-tools-mcp": "dist/index.js"
|
|
9
10
|
},
|
|
10
|
-
"type": "module",
|
|
11
11
|
"files": [
|
|
12
|
-
"dist
|
|
12
|
+
"dist",
|
|
13
13
|
"README.md",
|
|
14
|
-
"LICENSE"
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"src"
|
|
15
16
|
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20.0.0"
|
|
19
|
+
},
|
|
16
20
|
"scripts": {
|
|
17
|
-
"start": "node dist/index.js",
|
|
18
|
-
"dev": "bun --watch index.ts",
|
|
19
|
-
"build": "bun build index.ts --outdir dist --target node --format esm && chmod +x dist/index.js",
|
|
20
21
|
"clean": "rm -rf dist",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"build": "npm run clean && tsc",
|
|
23
|
+
"watch": "tsc --watch",
|
|
24
|
+
"prepare": "npm run build",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"test:ui": "vitest --ui"
|
|
24
28
|
},
|
|
25
|
-
"keywords": [
|
|
29
|
+
"keywords": [
|
|
30
|
+
"mcp",
|
|
31
|
+
"ethereum",
|
|
32
|
+
"viem",
|
|
33
|
+
"blockchain",
|
|
34
|
+
"web3",
|
|
35
|
+
"ens",
|
|
36
|
+
"ethereum-name-service",
|
|
37
|
+
"model-context-protocol",
|
|
38
|
+
"ai"
|
|
39
|
+
],
|
|
26
40
|
"author": "Benjamin Hajnal <hajnalbenjamin@gmail.com>",
|
|
27
41
|
"license": "MIT",
|
|
28
42
|
"repository": {
|
|
@@ -33,16 +47,17 @@
|
|
|
33
47
|
"url": "https://github.com/hajnalben/web3-tools-mcp/issues"
|
|
34
48
|
},
|
|
35
49
|
"homepage": "https://github.com/hajnalben/web3-tools-mcp#readme",
|
|
36
|
-
"devDependencies": {
|
|
37
|
-
"@types/bun": "latest",
|
|
38
|
-
"@types/node": "^24.0.4"
|
|
39
|
-
},
|
|
40
|
-
"peerDependencies": {
|
|
41
|
-
"typescript": "^5"
|
|
42
|
-
},
|
|
43
50
|
"dependencies": {
|
|
44
51
|
"@envio-dev/hypersync-client": "^0.6.5",
|
|
45
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
46
|
-
"viem": "^2.31.4"
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.20.1",
|
|
53
|
+
"viem": "^2.31.4",
|
|
54
|
+
"zod": "^3.25.76"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^20.19.23",
|
|
58
|
+
"@vitest/ui": "^3.2.4",
|
|
59
|
+
"dotenv": "^17.2.3",
|
|
60
|
+
"typescript": "^5.0.0",
|
|
61
|
+
"vitest": "^3.2.4"
|
|
47
62
|
}
|
|
48
63
|
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { http, createPublicClient } from 'viem'
|
|
2
|
+
import { arbitrum, base, celo, localhost, mainnet, optimism, polygon } from 'viem/chains'
|
|
3
|
+
import type { ChainName, Config } from './types.js'
|
|
4
|
+
|
|
5
|
+
// Enhanced RPC configuration with API keys
|
|
6
|
+
function getRpcUrl(chainName: string, config: Config): string {
|
|
7
|
+
// Check for custom RPC URLs first
|
|
8
|
+
if (config.customRpcUrls?.[chainName]) {
|
|
9
|
+
return config.customRpcUrls[chainName]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Enhanced RPC URLs with API keys
|
|
13
|
+
const enhancedRpcs: Record<string, string> = {
|
|
14
|
+
mainnet: config.alchemyApiKey
|
|
15
|
+
? `https://eth-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
|
|
16
|
+
: config.infuraApiKey
|
|
17
|
+
? `https://mainnet.infura.io/v3/${config.infuraApiKey}`
|
|
18
|
+
: 'https://eth.llamarpc.com',
|
|
19
|
+
|
|
20
|
+
base: config.alchemyApiKey
|
|
21
|
+
? `https://base-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
|
|
22
|
+
: 'https://base.llamarpc.com',
|
|
23
|
+
|
|
24
|
+
arbitrum: config.alchemyApiKey
|
|
25
|
+
? `https://arb-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
|
|
26
|
+
: config.infuraApiKey
|
|
27
|
+
? `https://arbitrum-mainnet.infura.io/v3/${config.infuraApiKey}`
|
|
28
|
+
: 'https://arb1.arbitrum.io/rpc',
|
|
29
|
+
|
|
30
|
+
polygon: config.alchemyApiKey
|
|
31
|
+
? `https://polygon-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
|
|
32
|
+
: config.infuraApiKey
|
|
33
|
+
? `https://polygon-mainnet.infura.io/v3/${config.infuraApiKey}`
|
|
34
|
+
: 'https://polygon-rpc.com/',
|
|
35
|
+
|
|
36
|
+
optimism: config.alchemyApiKey
|
|
37
|
+
? `https://opt-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`
|
|
38
|
+
: config.infuraApiKey
|
|
39
|
+
? `https://optimism-mainnet.infura.io/v3/${config.infuraApiKey}`
|
|
40
|
+
: 'https://mainnet.optimism.io',
|
|
41
|
+
|
|
42
|
+
celo: 'https://celo.drpc.org',
|
|
43
|
+
localhost: 'http://localhost:8545'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return enhancedRpcs[chainName] ?? 'https://eth.llamarpc.com'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Supported chains with their RPC endpoints (now dynamic)
|
|
50
|
+
export const CHAINS = (config: Config) => ({
|
|
51
|
+
mainnet: {
|
|
52
|
+
chain: mainnet,
|
|
53
|
+
get rpc() {
|
|
54
|
+
return getRpcUrl('mainnet', config)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
base: {
|
|
58
|
+
chain: base,
|
|
59
|
+
get rpc() {
|
|
60
|
+
return getRpcUrl('base', config)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
arbitrum: {
|
|
64
|
+
chain: arbitrum,
|
|
65
|
+
get rpc() {
|
|
66
|
+
return getRpcUrl('arbitrum', config)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
polygon: {
|
|
70
|
+
chain: polygon,
|
|
71
|
+
get rpc() {
|
|
72
|
+
return getRpcUrl('polygon', config)
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
optimism: {
|
|
76
|
+
chain: optimism,
|
|
77
|
+
get rpc() {
|
|
78
|
+
return getRpcUrl('optimism', config)
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
celo: {
|
|
82
|
+
chain: celo,
|
|
83
|
+
get rpc() {
|
|
84
|
+
return getRpcUrl('celo', config)
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
localhost: {
|
|
88
|
+
chain: localhost,
|
|
89
|
+
get rpc() {
|
|
90
|
+
return getRpcUrl('localhost', config)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Hypersync URLs for supported chains (localhost is not supported)
|
|
96
|
+
export const HYPERSYNC_URLS: Partial<Record<ChainName, string>> = {
|
|
97
|
+
base: 'https://base.hypersync.xyz',
|
|
98
|
+
arbitrum: 'https://arbitrum.hypersync.xyz',
|
|
99
|
+
mainnet: 'https://eth.hypersync.xyz',
|
|
100
|
+
polygon: 'https://polygon.hypersync.xyz',
|
|
101
|
+
optimism: 'https://optimism.hypersync.xyz'
|
|
102
|
+
// celo and localhost are not supported by hypersync
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const SUPPORTED_CHAINS = ['mainnet', 'base', 'arbitrum', 'polygon', 'optimism', 'celo', 'localhost'] as const
|
|
106
|
+
|
|
107
|
+
// Client manager
|
|
108
|
+
export class ClientManager {
|
|
109
|
+
private clients: Map<ChainName, unknown> = new Map()
|
|
110
|
+
private config: Config
|
|
111
|
+
|
|
112
|
+
constructor(config: Config) {
|
|
113
|
+
this.config = config
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getClient(chainName: ChainName) {
|
|
117
|
+
if (!this.clients.has(chainName)) {
|
|
118
|
+
const chains = CHAINS(this.config)
|
|
119
|
+
const { chain, rpc } = chains[chainName]
|
|
120
|
+
const client = createPublicClient({
|
|
121
|
+
chain,
|
|
122
|
+
transport: http(rpc)
|
|
123
|
+
})
|
|
124
|
+
this.clients.set(chainName, client)
|
|
125
|
+
}
|
|
126
|
+
const client = this.clients.get(chainName)
|
|
127
|
+
if (!client) {
|
|
128
|
+
throw new Error(`Failed to create client for chain: ${chainName}`)
|
|
129
|
+
}
|
|
130
|
+
return client as ReturnType<typeof createPublicClient>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getEtherscanDomain(chainName: ChainName): string {
|
|
134
|
+
const domains: Record<ChainName, string> = {
|
|
135
|
+
mainnet: 'etherscan.io',
|
|
136
|
+
base: 'basescan.org',
|
|
137
|
+
arbitrum: 'arbiscan.io',
|
|
138
|
+
polygon: 'polygonscan.com',
|
|
139
|
+
optimism: 'optimistic.etherscan.io',
|
|
140
|
+
celo: 'celoscan.io',
|
|
141
|
+
localhost: 'etherscan.io'
|
|
142
|
+
}
|
|
143
|
+
return domains[chainName] || 'etherscan.io'
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Export a global client manager instance
|
|
148
|
+
let clientManager: ClientManager | null = null
|
|
149
|
+
|
|
150
|
+
export function initializeClientManager(config: Config): ClientManager {
|
|
151
|
+
clientManager = new ClientManager(config)
|
|
152
|
+
return clientManager
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getClientManager(): ClientManager {
|
|
156
|
+
if (!clientManager) {
|
|
157
|
+
throw new Error('Client manager not initialized')
|
|
158
|
+
}
|
|
159
|
+
return clientManager
|
|
160
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { initializeClientManager } from "./client.js";
|
|
7
|
+
import { registerAllTools } from "./tools/index.js";
|
|
8
|
+
import { parseCommandLineArgs } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
// Parse configuration
|
|
11
|
+
const config = parseCommandLineArgs();
|
|
12
|
+
|
|
13
|
+
// Show help if requested
|
|
14
|
+
if (config.showHelp) {
|
|
15
|
+
console.log(`
|
|
16
|
+
Web3 Tools MCP Server v${packageJson.version}
|
|
17
|
+
${packageJson.description}
|
|
18
|
+
|
|
19
|
+
USAGE:
|
|
20
|
+
npx web3-tools-mcp [OPTIONS]
|
|
21
|
+
|
|
22
|
+
OPTIONS:
|
|
23
|
+
--help, -h Show this help message
|
|
24
|
+
--etherscan-api-key <key> Etherscan API key (for contract ABI retrieval)
|
|
25
|
+
--alchemy-api-key <key> Alchemy API key (for enhanced RPC)
|
|
26
|
+
--infura-api-key <key> Infura API key (for alternative RPC)
|
|
27
|
+
--hypersync-api-key <key> Hypersync API key (for fast event queries)
|
|
28
|
+
--custom-rpc <json> Custom RPC URLs as JSON object
|
|
29
|
+
Example: '{"mainnet":"https://...", "base":"https://..."}'
|
|
30
|
+
|
|
31
|
+
ENVIRONMENT VARIABLES:
|
|
32
|
+
ETHERSCAN_API_KEY Alternative to --etherscan-api-key
|
|
33
|
+
ALCHEMY_API_KEY Alternative to --alchemy-api-key
|
|
34
|
+
INFURA_API_KEY Alternative to --infura-api-key
|
|
35
|
+
HYPERSYNC_API_KEY Alternative to --hypersync-api-key
|
|
36
|
+
|
|
37
|
+
SUPPORTED CHAINS:
|
|
38
|
+
mainnet, base, arbitrum, polygon, optimism, celo, localhost
|
|
39
|
+
|
|
40
|
+
EXAMPLES:
|
|
41
|
+
# Use with npx (recommended)
|
|
42
|
+
npx web3-tools-mcp --etherscan-api-key YOUR_KEY
|
|
43
|
+
|
|
44
|
+
# Use environment variables
|
|
45
|
+
export ETHERSCAN_API_KEY=your_key
|
|
46
|
+
npx web3-tools-mcp
|
|
47
|
+
|
|
48
|
+
# Use custom RPC
|
|
49
|
+
npx web3-tools-mcp --custom-rpc '{"mainnet":"https://my-rpc.com"}'
|
|
50
|
+
|
|
51
|
+
DOCUMENTATION:
|
|
52
|
+
GitHub: ${packageJson.repository.url.replace('git+', '').replace('.git', '')}
|
|
53
|
+
Issues: ${packageJson.bugs.url}
|
|
54
|
+
|
|
55
|
+
For MCP client configuration, see the README.md file.
|
|
56
|
+
`);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Log configuration info
|
|
61
|
+
if (config.etherscanApiKey) {
|
|
62
|
+
console.error("[MCP] Etherscan API key configured");
|
|
63
|
+
}
|
|
64
|
+
if (config.alchemyApiKey) {
|
|
65
|
+
console.error("[MCP] Alchemy API key configured");
|
|
66
|
+
}
|
|
67
|
+
if (config.infuraApiKey) {
|
|
68
|
+
console.error("[MCP] Infura API key configured");
|
|
69
|
+
}
|
|
70
|
+
if (config.customRpcUrls) {
|
|
71
|
+
console.error("[MCP] Custom RPC URLs:", Object.keys(config.customRpcUrls));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Initialize client manager
|
|
75
|
+
initializeClientManager(config);
|
|
76
|
+
|
|
77
|
+
// Create MCP server instance
|
|
78
|
+
const server = new McpServer({
|
|
79
|
+
name: "web3-tools-mcp",
|
|
80
|
+
version: packageJson.version,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Register all tools
|
|
84
|
+
registerAllTools(server);
|
|
85
|
+
|
|
86
|
+
// Start server
|
|
87
|
+
async function main() {
|
|
88
|
+
const transport = new StdioServerTransport();
|
|
89
|
+
await server.connect(transport);
|
|
90
|
+
console.error("Web3 Tools MCP Server running on stdio");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
main().catch((error) => {
|
|
94
|
+
console.error("Fatal error:", error);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { type Address, decodeAbiParameters, isAddress, parseAbiParameters } from 'viem'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { ChainName } from '../types.js'
|
|
4
|
+
import { getClientManager, SUPPORTED_CHAINS } from '../client.js'
|
|
5
|
+
import { createTool, formatResponse } from '../utils.js'
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
get_storage_at: createTool(
|
|
9
|
+
'Read Contract Storage',
|
|
10
|
+
'⚠️ ADVANCED: Direct storage slot access with type decoding. Use for low-level contract inspection.',
|
|
11
|
+
z.object({
|
|
12
|
+
chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
|
|
13
|
+
address: z.string().describe('The contract address to inspect'),
|
|
14
|
+
slot: z.string().describe('Storage slot (hex string, e.g., "0x0")'),
|
|
15
|
+
abiType: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe('ABI type for decoding (e.g., "uint256", "address", "bool", "bytes32")'),
|
|
18
|
+
blockNumber: z.string().optional().describe('Block number (optional, defaults to latest)')
|
|
19
|
+
}),
|
|
20
|
+
async (args) => {
|
|
21
|
+
if (!isAddress(args.address)) {
|
|
22
|
+
throw new Error('Invalid address format')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const clientManager = getClientManager()
|
|
26
|
+
const client = clientManager.getClient(args.chain as ChainName)
|
|
27
|
+
const blockTag = args.blockNumber ? BigInt(args.blockNumber) : 'latest'
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const storageValue = await client.getStorageAt({
|
|
31
|
+
address: args.address as Address,
|
|
32
|
+
slot: args.slot as `0x${string}`,
|
|
33
|
+
blockNumber: blockTag === 'latest' ? undefined : blockTag
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (!storageValue) {
|
|
37
|
+
throw new Error('No storage value found')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Decode based on ABI type
|
|
41
|
+
let decodedValue: unknown
|
|
42
|
+
let formattedValue: string
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
switch (args.abiType.toLowerCase()) {
|
|
46
|
+
case 'uint256':
|
|
47
|
+
case 'uint': {
|
|
48
|
+
decodedValue = BigInt(storageValue)
|
|
49
|
+
formattedValue = (decodedValue as bigint).toString()
|
|
50
|
+
break
|
|
51
|
+
}
|
|
52
|
+
case 'int256':
|
|
53
|
+
case 'int': {
|
|
54
|
+
// Handle two's complement for negative numbers
|
|
55
|
+
const uint256Value = BigInt(storageValue)
|
|
56
|
+
const maxUint256 = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
|
|
57
|
+
decodedValue = uint256Value > maxUint256 / 2n ? uint256Value - maxUint256 - 1n : uint256Value
|
|
58
|
+
formattedValue = (decodedValue as bigint).toString()
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
case 'address': {
|
|
62
|
+
decodedValue = `0x${storageValue.slice(-40)}`
|
|
63
|
+
formattedValue = decodedValue as string
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
case 'bool': {
|
|
67
|
+
decodedValue = BigInt(storageValue) !== 0n
|
|
68
|
+
formattedValue = String(decodedValue)
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
case 'bytes32': {
|
|
72
|
+
decodedValue = storageValue
|
|
73
|
+
formattedValue = storageValue
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
default: {
|
|
77
|
+
// Try to decode as ABI parameters
|
|
78
|
+
try {
|
|
79
|
+
const parsedTypes = parseAbiParameters([args.abiType])
|
|
80
|
+
if (parsedTypes.length > 0) {
|
|
81
|
+
const decoded = decodeAbiParameters(parsedTypes, storageValue) as readonly unknown[]
|
|
82
|
+
decodedValue = decoded.length > 0 ? decoded[0] : null
|
|
83
|
+
formattedValue = decodedValue ? String(decodedValue) : 'No data'
|
|
84
|
+
} else {
|
|
85
|
+
decodedValue = null
|
|
86
|
+
formattedValue = 'Invalid ABI type'
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
decodedValue = null
|
|
90
|
+
formattedValue = 'Failed to decode with custom ABI type'
|
|
91
|
+
}
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
decodedValue = null
|
|
97
|
+
formattedValue = 'Failed to decode'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return formatResponse({
|
|
101
|
+
rawValue: storageValue,
|
|
102
|
+
decodedValue,
|
|
103
|
+
formattedValue,
|
|
104
|
+
abiType: args.abiType,
|
|
105
|
+
slot: args.slot,
|
|
106
|
+
address: args.address,
|
|
107
|
+
chain: args.chain,
|
|
108
|
+
blockNumber: blockTag === 'latest' ? 'latest' : blockTag.toString()
|
|
109
|
+
})
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new Error(`Failed to get storage: ${error}`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
|
|
116
|
+
get_block_info: createTool(
|
|
117
|
+
'Get Block Information',
|
|
118
|
+
'Retrieve block data including timestamps, hashes, and dates. Use for time-based analysis.',
|
|
119
|
+
z.object({
|
|
120
|
+
chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
|
|
121
|
+
blockNumber: z.string().optional().describe('Block number (optional, defaults to latest)')
|
|
122
|
+
}),
|
|
123
|
+
async (args) => {
|
|
124
|
+
const clientManager = getClientManager()
|
|
125
|
+
const client = clientManager.getClient(args.chain as ChainName)
|
|
126
|
+
const blockTag = args.blockNumber ? BigInt(args.blockNumber) : 'latest'
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const block = await client.getBlock({
|
|
130
|
+
blockNumber: blockTag === 'latest' ? undefined : blockTag,
|
|
131
|
+
includeTransactions: false
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const timestamp = Number(block.timestamp)
|
|
135
|
+
const date = new Date(timestamp * 1000)
|
|
136
|
+
|
|
137
|
+
return formatResponse({
|
|
138
|
+
blockNumber: block.number,
|
|
139
|
+
timestamp: block.timestamp,
|
|
140
|
+
timestampMs: timestamp * 1000,
|
|
141
|
+
dateIso: date.toISOString(),
|
|
142
|
+
dateReadable: date.toLocaleString(),
|
|
143
|
+
hash: block.hash,
|
|
144
|
+
parentHash: block.parentHash,
|
|
145
|
+
chain: args.chain
|
|
146
|
+
})
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new Error(`Failed to get block time: ${error}`)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
),
|
|
152
|
+
|
|
153
|
+
trace_transaction: createTool(
|
|
154
|
+
'Trace Transaction',
|
|
155
|
+
'⚠️ INTENSIVE: Detailed transaction analysis including call traces and state changes. Use sparingly.',
|
|
156
|
+
z.object({
|
|
157
|
+
chain: z.enum(SUPPORTED_CHAINS).describe('The blockchain network to use'),
|
|
158
|
+
transactionHash: z.string().describe('Transaction hash to trace (0x prefixed)'),
|
|
159
|
+
traceType: z
|
|
160
|
+
.enum(['trace', 'vmTrace', 'stateDiff'])
|
|
161
|
+
.optional()
|
|
162
|
+
.describe(
|
|
163
|
+
'Trace type: "trace" (call tree, recommended), "vmTrace" (VM execution), "stateDiff" (state changes)'
|
|
164
|
+
)
|
|
165
|
+
.default('trace')
|
|
166
|
+
}),
|
|
167
|
+
async (args) => {
|
|
168
|
+
const clientManager = getClientManager()
|
|
169
|
+
const client = clientManager.getClient(args.chain as ChainName)
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// First get the transaction receipt to make sure it exists
|
|
173
|
+
const transaction = await client.getTransaction({ hash: args.transactionHash as `0x${string}` })
|
|
174
|
+
const receipt = await client.getTransactionReceipt({ hash: args.transactionHash as `0x${string}` })
|
|
175
|
+
|
|
176
|
+
let traceResult: unknown = null
|
|
177
|
+
|
|
178
|
+
// Perform the requested trace type
|
|
179
|
+
switch (args.traceType) {
|
|
180
|
+
case 'trace':
|
|
181
|
+
try {
|
|
182
|
+
// Use debug_traceTransaction for call trace
|
|
183
|
+
traceResult = await client.request({
|
|
184
|
+
method: 'debug_traceTransaction',
|
|
185
|
+
params: [args.transactionHash, { tracer: 'callTracer' }]
|
|
186
|
+
})
|
|
187
|
+
} catch (e) {
|
|
188
|
+
traceResult = { error: (e as Error).message }
|
|
189
|
+
}
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
case 'vmTrace':
|
|
193
|
+
try {
|
|
194
|
+
traceResult = await client.request({
|
|
195
|
+
method: 'debug_traceTransaction',
|
|
196
|
+
params: [args.transactionHash, { tracer: 'prestateTracer' }]
|
|
197
|
+
})
|
|
198
|
+
} catch (e) {
|
|
199
|
+
traceResult = { error: (e as Error).message }
|
|
200
|
+
}
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
case 'stateDiff':
|
|
204
|
+
try {
|
|
205
|
+
traceResult = await client.request({
|
|
206
|
+
method: 'debug_traceTransaction',
|
|
207
|
+
params: [args.transactionHash, { tracer: 'stateDiffTracer' }]
|
|
208
|
+
})
|
|
209
|
+
} catch (e) {
|
|
210
|
+
traceResult = { error: (e as Error).message }
|
|
211
|
+
}
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
default:
|
|
215
|
+
traceResult = {
|
|
216
|
+
type: 'CALL',
|
|
217
|
+
from: transaction.from,
|
|
218
|
+
to: transaction.to,
|
|
219
|
+
value: transaction.value?.toString() || '0',
|
|
220
|
+
gas: transaction.gas?.toString() || '0',
|
|
221
|
+
gasUsed: receipt.gasUsed?.toString() || '0',
|
|
222
|
+
input: transaction.input,
|
|
223
|
+
output: '0x',
|
|
224
|
+
error: receipt.status === 'success' ? null : 'Transaction failed'
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const result = {
|
|
229
|
+
success: true,
|
|
230
|
+
chain: args.chain,
|
|
231
|
+
transactionHash: args.transactionHash,
|
|
232
|
+
traceType: args.traceType,
|
|
233
|
+
transaction: {
|
|
234
|
+
blockNumber: transaction.blockNumber?.toString(),
|
|
235
|
+
from: transaction.from,
|
|
236
|
+
to: transaction.to,
|
|
237
|
+
value: transaction.value?.toString() || '0',
|
|
238
|
+
gas: transaction.gas?.toString() || '0',
|
|
239
|
+
gasPrice: transaction.gasPrice?.toString() || '0',
|
|
240
|
+
nonce: transaction.nonce?.toString() || '0',
|
|
241
|
+
input: transaction.input
|
|
242
|
+
},
|
|
243
|
+
receipt: {
|
|
244
|
+
status: receipt.status,
|
|
245
|
+
gasUsed: receipt.gasUsed?.toString() || '0',
|
|
246
|
+
effectiveGasPrice: receipt.effectiveGasPrice?.toString() || '0',
|
|
247
|
+
logs: receipt.logs.map(log => ({
|
|
248
|
+
address: log.address,
|
|
249
|
+
topics: log.topics,
|
|
250
|
+
data: log.data
|
|
251
|
+
}))
|
|
252
|
+
},
|
|
253
|
+
trace: traceResult
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return formatResponse(result)
|
|
257
|
+
} catch (error) {
|
|
258
|
+
throw new Error(`Transaction trace failed: ${error}`)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
}
|