iflow-mcp-crazyrabbitltc-mcp-etherscan-server 1.0.1
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/LICENSE +21 -0
- package/README.md +124 -0
- package/build/index.js +6 -0
- package/build/server.js +270 -0
- package/build/services/etherscanService.js +134 -0
- package/build/services/exampleService.js +6 -0
- package/package.json +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# MCP Etherscan Server
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server that provides Ethereum blockchain data tools via Etherscan's API. Features include checking ETH balances, viewing transaction history, tracking ERC20 transfers, fetching contract ABIs, monitoring gas prices, and resolving ENS names.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Balance Checking**: Get ETH balance for any Ethereum address
|
|
8
|
+
- **Transaction History**: View recent transactions with detailed information
|
|
9
|
+
- **Token Transfers**: Track ERC20 token transfers with token details
|
|
10
|
+
- **Contract ABI**: Fetch smart contract ABIs for development
|
|
11
|
+
- **Gas Prices**: Monitor current gas prices (Safe Low, Standard, Fast)
|
|
12
|
+
- **ENS Resolution**: Resolve Ethereum addresses to ENS names
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
- Node.js >= 18
|
|
17
|
+
- An Etherscan API key (get one at https://etherscan.io/apis)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
1. Clone the repository:
|
|
22
|
+
```bash
|
|
23
|
+
git clone [your-repo-url]
|
|
24
|
+
cd mcp-etherscan-server
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. Install dependencies:
|
|
28
|
+
```bash
|
|
29
|
+
npm install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. Create a `.env` file in the root directory:
|
|
33
|
+
```bash
|
|
34
|
+
ETHERSCAN_API_KEY=your_api_key_here
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
4. Build the project:
|
|
38
|
+
```bash
|
|
39
|
+
npm run build
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Running the Server
|
|
43
|
+
|
|
44
|
+
Start the server:
|
|
45
|
+
```bash
|
|
46
|
+
npm start
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The server will run on stdio, making it compatible with MCP clients like Claude Desktop.
|
|
50
|
+
|
|
51
|
+
## How It Works
|
|
52
|
+
|
|
53
|
+
This server implements the Model Context Protocol (MCP) to provide tools for interacting with Ethereum blockchain data through Etherscan's API. Each tool is exposed as an MCP endpoint that can be called by compatible clients.
|
|
54
|
+
|
|
55
|
+
### Available Tools
|
|
56
|
+
|
|
57
|
+
1. `check-balance`
|
|
58
|
+
- Input: Ethereum address
|
|
59
|
+
- Output: ETH balance in both Wei and ETH
|
|
60
|
+
|
|
61
|
+
2. `get-transactions`
|
|
62
|
+
- Input: Ethereum address, optional limit
|
|
63
|
+
- Output: Recent transactions with timestamps, values, and addresses
|
|
64
|
+
|
|
65
|
+
3. `get-token-transfers`
|
|
66
|
+
- Input: Ethereum address, optional limit
|
|
67
|
+
- Output: Recent ERC20 token transfers with token details
|
|
68
|
+
|
|
69
|
+
4. `get-contract-abi`
|
|
70
|
+
- Input: Contract address
|
|
71
|
+
- Output: Contract ABI in JSON format
|
|
72
|
+
|
|
73
|
+
5. `get-gas-prices`
|
|
74
|
+
- Input: None
|
|
75
|
+
- Output: Current gas prices in Gwei
|
|
76
|
+
|
|
77
|
+
6. `get-ens-name`
|
|
78
|
+
- Input: Ethereum address
|
|
79
|
+
- Output: Associated ENS name if available
|
|
80
|
+
|
|
81
|
+
## Using with Claude Desktop
|
|
82
|
+
|
|
83
|
+
To add this server to Claude Desktop:
|
|
84
|
+
|
|
85
|
+
1. Start the server using `npm start`
|
|
86
|
+
|
|
87
|
+
2. In Claude Desktop:
|
|
88
|
+
- Go to Settings
|
|
89
|
+
- Navigate to the MCP Servers section
|
|
90
|
+
- Click "Add Server"
|
|
91
|
+
- Enter the following configuration:
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"name": "Etherscan Tools",
|
|
95
|
+
"transport": "stdio",
|
|
96
|
+
"command": "node /path/to/mcp-etherscan-server/build/index.js"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
- Save the configuration
|
|
100
|
+
|
|
101
|
+
3. The Etherscan tools will now be available in your Claude conversations
|
|
102
|
+
|
|
103
|
+
### Example Usage in Claude
|
|
104
|
+
|
|
105
|
+
You can use commands like:
|
|
106
|
+
```
|
|
107
|
+
Check the balance of 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
|
|
108
|
+
```
|
|
109
|
+
or
|
|
110
|
+
```
|
|
111
|
+
Show me recent transactions for vitalik.eth
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
To add new features or modify existing ones:
|
|
117
|
+
|
|
118
|
+
1. The main server logic is in `src/server.ts`
|
|
119
|
+
2. Etherscan API interactions are handled in `src/services/etherscanService.ts`
|
|
120
|
+
3. Build after changes: `npm run build`
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT License - See LICENSE file for details
|
package/build/index.js
ADDED
package/build/server.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { config } from 'dotenv';
|
|
5
|
+
import { EtherscanService } from './services/etherscanService.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
// Load environment variables
|
|
8
|
+
config();
|
|
9
|
+
const apiKey = process.env.ETHERSCAN_API_KEY;
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
throw new Error('ETHERSCAN_API_KEY environment variable is required');
|
|
12
|
+
}
|
|
13
|
+
// Initialize Etherscan service
|
|
14
|
+
const etherscanService = new EtherscanService(apiKey);
|
|
15
|
+
// Create server instance
|
|
16
|
+
const server = new Server({
|
|
17
|
+
name: "etherscan-server",
|
|
18
|
+
version: "1.0.0",
|
|
19
|
+
}, {
|
|
20
|
+
capabilities: {
|
|
21
|
+
tools: {},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
// Define schemas for validation
|
|
25
|
+
const AddressSchema = z.object({
|
|
26
|
+
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
|
|
27
|
+
});
|
|
28
|
+
const TransactionHistorySchema = z.object({
|
|
29
|
+
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
|
|
30
|
+
limit: z.number().min(1).max(100).optional(),
|
|
31
|
+
});
|
|
32
|
+
const TokenTransferSchema = z.object({
|
|
33
|
+
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
|
|
34
|
+
limit: z.number().min(1).max(100).optional(),
|
|
35
|
+
});
|
|
36
|
+
const ContractSchema = z.object({
|
|
37
|
+
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
|
|
38
|
+
});
|
|
39
|
+
// List available tools
|
|
40
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
41
|
+
return {
|
|
42
|
+
tools: [
|
|
43
|
+
{
|
|
44
|
+
name: "check-balance",
|
|
45
|
+
description: "Check the ETH balance of an Ethereum address",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
address: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Ethereum address (0x format)",
|
|
52
|
+
pattern: "^0x[a-fA-F0-9]{40}$"
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ["address"],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "get-transactions",
|
|
60
|
+
description: "Get recent transactions for an Ethereum address",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
address: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Ethereum address (0x format)",
|
|
67
|
+
pattern: "^0x[a-fA-F0-9]{40}$"
|
|
68
|
+
},
|
|
69
|
+
limit: {
|
|
70
|
+
type: "number",
|
|
71
|
+
description: "Number of transactions to return (max 100)",
|
|
72
|
+
minimum: 1,
|
|
73
|
+
maximum: 100
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ["address"],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "get-token-transfers",
|
|
81
|
+
description: "Get ERC20 token transfers for an Ethereum address",
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
address: {
|
|
86
|
+
type: "string",
|
|
87
|
+
description: "Ethereum address (0x format)",
|
|
88
|
+
pattern: "^0x[a-fA-F0-9]{40}$"
|
|
89
|
+
},
|
|
90
|
+
limit: {
|
|
91
|
+
type: "number",
|
|
92
|
+
description: "Number of transfers to return (max 100)",
|
|
93
|
+
minimum: 1,
|
|
94
|
+
maximum: 100
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
required: ["address"],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "get-contract-abi",
|
|
102
|
+
description: "Get the ABI for a smart contract",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
address: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "Contract address (0x format)",
|
|
109
|
+
pattern: "^0x[a-fA-F0-9]{40}$"
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ["address"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "get-gas-prices",
|
|
117
|
+
description: "Get current gas prices in Gwei",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "get-ens-name",
|
|
125
|
+
description: "Get the ENS name for an Ethereum address",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
address: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "Ethereum address (0x format)",
|
|
132
|
+
pattern: "^0x[a-fA-F0-9]{40}$"
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
required: ["address"],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
// Handle tool execution
|
|
142
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
143
|
+
const { name, arguments: args } = request.params;
|
|
144
|
+
if (name === "check-balance") {
|
|
145
|
+
try {
|
|
146
|
+
const { address } = AddressSchema.parse(args);
|
|
147
|
+
const balance = await etherscanService.getAddressBalance(address);
|
|
148
|
+
const response = `Address: ${balance.address}\nBalance: ${balance.balanceInEth} ETH`;
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: response }],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error instanceof z.ZodError) {
|
|
155
|
+
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (name === "get-transactions") {
|
|
161
|
+
try {
|
|
162
|
+
const { address, limit } = TransactionHistorySchema.parse(args);
|
|
163
|
+
const transactions = await etherscanService.getTransactionHistory(address, limit);
|
|
164
|
+
const formattedTransactions = transactions.map(tx => {
|
|
165
|
+
const date = new Date(tx.timestamp * 1000).toLocaleString();
|
|
166
|
+
return `Block ${tx.blockNumber} (${date}):\n` +
|
|
167
|
+
`Hash: ${tx.hash}\n` +
|
|
168
|
+
`From: ${tx.from}\n` +
|
|
169
|
+
`To: ${tx.to}\n` +
|
|
170
|
+
`Value: ${tx.value} ETH\n` +
|
|
171
|
+
`---`;
|
|
172
|
+
}).join('\n');
|
|
173
|
+
const response = transactions.length > 0
|
|
174
|
+
? `Recent transactions for ${address}:\n\n${formattedTransactions}`
|
|
175
|
+
: `No transactions found for ${address}`;
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: "text", text: response }],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error instanceof z.ZodError) {
|
|
182
|
+
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
|
|
183
|
+
}
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (name === "get-token-transfers") {
|
|
188
|
+
try {
|
|
189
|
+
const { address, limit } = TokenTransferSchema.parse(args);
|
|
190
|
+
const transfers = await etherscanService.getTokenTransfers(address, limit);
|
|
191
|
+
const formattedTransfers = transfers.map(tx => {
|
|
192
|
+
const date = new Date(tx.timestamp * 1000).toLocaleString();
|
|
193
|
+
return `Block ${tx.blockNumber} (${date}):\n` +
|
|
194
|
+
`Token: ${tx.tokenName} (${tx.tokenSymbol})\n` +
|
|
195
|
+
`From: ${tx.from}\n` +
|
|
196
|
+
`To: ${tx.to}\n` +
|
|
197
|
+
`Value: ${tx.value}\n` +
|
|
198
|
+
`Contract: ${tx.token}\n` +
|
|
199
|
+
`---`;
|
|
200
|
+
}).join('\n');
|
|
201
|
+
const response = transfers.length > 0
|
|
202
|
+
? `Recent token transfers for ${address}:\n\n${formattedTransfers}`
|
|
203
|
+
: `No token transfers found for ${address}`;
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text", text: response }],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (error instanceof z.ZodError) {
|
|
210
|
+
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (name === "get-contract-abi") {
|
|
216
|
+
try {
|
|
217
|
+
const { address } = ContractSchema.parse(args);
|
|
218
|
+
const abi = await etherscanService.getContractABI(address);
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text: `Contract ABI for ${address}:\n\n${abi}` }],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (error instanceof z.ZodError) {
|
|
225
|
+
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
|
|
226
|
+
}
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (name === "get-gas-prices") {
|
|
231
|
+
try {
|
|
232
|
+
const prices = await etherscanService.getGasOracle();
|
|
233
|
+
const response = `Current Gas Prices:\n` +
|
|
234
|
+
`Safe Low: ${prices.safeGwei} Gwei\n` +
|
|
235
|
+
`Standard: ${prices.proposeGwei} Gwei\n` +
|
|
236
|
+
`Fast: ${prices.fastGwei} Gwei`;
|
|
237
|
+
return {
|
|
238
|
+
content: [{ type: "text", text: response }],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (name === "get-ens-name") {
|
|
246
|
+
try {
|
|
247
|
+
const { address } = AddressSchema.parse(args);
|
|
248
|
+
const ensName = await etherscanService.getENSName(address);
|
|
249
|
+
const response = ensName
|
|
250
|
+
? `ENS name for ${address}: ${ensName}`
|
|
251
|
+
: `No ENS name found for ${address}`;
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: "text", text: response }],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error instanceof z.ZodError) {
|
|
258
|
+
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
264
|
+
});
|
|
265
|
+
// Start the server
|
|
266
|
+
export async function startServer() {
|
|
267
|
+
const transport = new StdioServerTransport();
|
|
268
|
+
await server.connect(transport);
|
|
269
|
+
console.error("Etherscan MCP Server running on stdio");
|
|
270
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
|
+
export class EtherscanService {
|
|
3
|
+
provider;
|
|
4
|
+
constructor(apiKey) {
|
|
5
|
+
this.provider = new ethers.EtherscanProvider('mainnet', apiKey);
|
|
6
|
+
}
|
|
7
|
+
async getAddressBalance(address) {
|
|
8
|
+
try {
|
|
9
|
+
// Validate the address
|
|
10
|
+
const validAddress = ethers.getAddress(address);
|
|
11
|
+
// Get balance in Wei
|
|
12
|
+
const balanceInWei = await this.provider.getBalance(validAddress);
|
|
13
|
+
// Convert to ETH
|
|
14
|
+
const balanceInEth = ethers.formatEther(balanceInWei);
|
|
15
|
+
return {
|
|
16
|
+
address: validAddress,
|
|
17
|
+
balanceInWei,
|
|
18
|
+
balanceInEth
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error instanceof Error) {
|
|
23
|
+
throw new Error(`Failed to get balance: ${error.message}`);
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async getTransactionHistory(address, limit = 10) {
|
|
29
|
+
try {
|
|
30
|
+
// Validate the address
|
|
31
|
+
const validAddress = ethers.getAddress(address);
|
|
32
|
+
// Get transactions directly from Etherscan API
|
|
33
|
+
const result = await fetch(`https://api.etherscan.io/api?module=account&action=txlist&address=${validAddress}&startblock=0&endblock=99999999&page=1&offset=${limit}&sort=desc&apikey=${this.provider.apiKey}`);
|
|
34
|
+
const data = await result.json();
|
|
35
|
+
if (data.status !== "1" || !data.result) {
|
|
36
|
+
throw new Error(data.message || "Failed to fetch transactions");
|
|
37
|
+
}
|
|
38
|
+
// Format the results
|
|
39
|
+
return data.result.slice(0, limit).map((tx) => ({
|
|
40
|
+
hash: tx.hash,
|
|
41
|
+
from: tx.from,
|
|
42
|
+
to: tx.to || 'Contract Creation',
|
|
43
|
+
value: ethers.formatEther(tx.value),
|
|
44
|
+
timestamp: parseInt(tx.timeStamp) || 0,
|
|
45
|
+
blockNumber: parseInt(tx.blockNumber) || 0
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error instanceof Error) {
|
|
50
|
+
throw new Error(`Failed to get transaction history: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async getTokenTransfers(address, limit = 10) {
|
|
56
|
+
try {
|
|
57
|
+
const validAddress = ethers.getAddress(address);
|
|
58
|
+
// Get ERC20 token transfers
|
|
59
|
+
const result = await fetch(`https://api.etherscan.io/api?module=account&action=tokentx&address=${validAddress}&page=1&offset=${limit}&sort=desc&apikey=${this.provider.apiKey}`);
|
|
60
|
+
const data = await result.json();
|
|
61
|
+
if (data.status !== "1" || !data.result) {
|
|
62
|
+
throw new Error(data.message || "Failed to fetch token transfers");
|
|
63
|
+
}
|
|
64
|
+
// Format the results
|
|
65
|
+
return data.result.slice(0, limit).map((tx) => ({
|
|
66
|
+
token: tx.contractAddress,
|
|
67
|
+
tokenName: tx.tokenName,
|
|
68
|
+
tokenSymbol: tx.tokenSymbol,
|
|
69
|
+
from: tx.from,
|
|
70
|
+
to: tx.to,
|
|
71
|
+
value: ethers.formatUnits(tx.value, parseInt(tx.tokenDecimal)),
|
|
72
|
+
timestamp: parseInt(tx.timeStamp) || 0,
|
|
73
|
+
blockNumber: parseInt(tx.blockNumber) || 0
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof Error) {
|
|
78
|
+
throw new Error(`Failed to get token transfers: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async getContractABI(address) {
|
|
84
|
+
try {
|
|
85
|
+
const validAddress = ethers.getAddress(address);
|
|
86
|
+
// Get contract ABI
|
|
87
|
+
const result = await fetch(`https://api.etherscan.io/api?module=contract&action=getabi&address=${validAddress}&apikey=${this.provider.apiKey}`);
|
|
88
|
+
const data = await result.json();
|
|
89
|
+
if (data.status !== "1" || !data.result) {
|
|
90
|
+
throw new Error(data.message || "Failed to fetch contract ABI");
|
|
91
|
+
}
|
|
92
|
+
return data.result;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
throw new Error(`Failed to get contract ABI: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async getGasOracle() {
|
|
102
|
+
try {
|
|
103
|
+
// Get current gas prices
|
|
104
|
+
const result = await fetch(`https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=${this.provider.apiKey}`);
|
|
105
|
+
const data = await result.json();
|
|
106
|
+
if (data.status !== "1" || !data.result) {
|
|
107
|
+
throw new Error(data.message || "Failed to fetch gas prices");
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
safeGwei: data.result.SafeGasPrice,
|
|
111
|
+
proposeGwei: data.result.ProposeGasPrice,
|
|
112
|
+
fastGwei: data.result.FastGasPrice
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof Error) {
|
|
117
|
+
throw new Error(`Failed to get gas prices: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async getENSName(address) {
|
|
123
|
+
try {
|
|
124
|
+
const validAddress = ethers.getAddress(address);
|
|
125
|
+
return await this.provider.lookupAddress(validAddress);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error instanceof Error) {
|
|
129
|
+
throw new Error(`Failed to get ENS name: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name": "iflow-mcp-crazyrabbitltc-mcp-etherscan-server", "version": "1.0.1", "description": "A TypeScript server boilerplate", "type": "module", "main": "build/index.js", "types": "build/index.d.ts", "bin": {"iflow-mcp-crazyrabbitltc-mcp-etherscan-server": "build/index.js"}, "scripts": {"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", "prepublishOnly": "npm run build", "start": "node build/index.js"}, "files": ["build", "README.md", "LICENSE"], "keywords": ["typescript", "server", "boilerplate"], "author": "", "license": "MIT", "dependencies": {"@modelcontextprotocol/sdk": "^1.0.0", "dotenv": "^16.0.0", "ethers": "^6.9.0", "zod": "^3.0.0"}, "devDependencies": {"@types/node": "^20.0.0", "typescript": "^5.0.0"}, "engines": {"node": ">=18"}}
|