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 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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from './server.js';
3
+ startServer().catch((error) => {
4
+ console.error('Failed to start server:', error);
5
+ process.exit(1);
6
+ });
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ // Add your service logic here
2
+ export class ExampleService {
3
+ constructor() {
4
+ // Initialize your service
5
+ }
6
+ }
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"}}