From 0d6e1d5b26cac6100849aaa30a3779f2cf3ab3f6 Mon Sep 17 00:00:00 2001
From: Philippe d'Argent
Date: Fri, 21 Mar 2025 14:28:37 +0100
Subject: [PATCH 1/2] add tool to get Txs from etherscan
---
README.md | 25 ++++-
src/tools/etherscan/handlers.ts | 164 ++++++++++++++++++++++++++++++++
src/tools/etherscan/index.ts | 10 ++
src/tools/etherscan/schemas.ts | 11 +++
src/tools/index.ts | 2 +
5 files changed, 211 insertions(+), 1 deletion(-)
create mode 100644 src/tools/etherscan/handlers.ts
create mode 100644 src/tools/etherscan/index.ts
create mode 100644 src/tools/etherscan/schemas.ts
diff --git a/README.md b/README.md
index 1f7a3db..f489b59 100644
--- a/README.md
+++ b/README.md
@@ -138,6 +138,10 @@ COINBASE_PROJECT_ID=your_project_id
# OpenRouter API Key (optional for buying OpenRouter credits)
# You can obtain this from https://openrouter.ai/keys
OPENROUTER_API_KEY=your_openrouter_api_key
+
+# Etherscan API Key (optional)
+# You can obtain this from https://docs.etherscan.io/etherscan-v2/getting-started/getting-an-api-key
+ETHERSCAN_API_KEY=your_etherscan_api_key
```
## Testing
@@ -179,7 +183,8 @@ You can easily access this file via the Claude Desktop app by navigating to Clau
"COINBASE_API_PRIVATE_KEY": "your_private_key",
"SEED_PHRASE": "your seed phrase here",
"COINBASE_PROJECT_ID": "your_project_id",
- "OPENROUTER_API_KEY": "your_openrouter_api_key"
+ "OPENROUTER_API_KEY": "your_openrouter_api_key",
+ "ETHERSCAN_API_KEY": "your_etherscan_api_key"
},
"disabled": false,
"autoApprove": []
@@ -329,6 +334,24 @@ Example query to Claude:
> "Buy $20 worth of OpenRouter credits."
+### etherscan_address_transactions
+
+Gets a list of transactions for an address using Etherscan API.
+
+Parameters:
+
+- `address`: The address to get transactions for
+- `startblock`: Starting block number (defaults to 0)
+- `endblock`: Ending block number (defaults to latest)
+- `page`: Page number (defaults to 1)
+- `offset`: Number of transactions per page (1-1000, defaults to 5)
+- `sort`: Sort transactions by block number (asc or desc, defaults to desc)
+- `chainId`: The chain ID (defaults to chain the wallet is connected to)
+
+Example query to Claude:
+
+> "Show me the most recent transactions for address 0xc5102fE9359FD9a28f877a67E36B0F050d81a3CC."
+
## Security Considerations
- The configuration file contains sensitive information (API keys and seed phrases). Ensure it's properly secured and not shared.
diff --git a/src/tools/etherscan/handlers.ts b/src/tools/etherscan/handlers.ts
new file mode 100644
index 0000000..6317606
--- /dev/null
+++ b/src/tools/etherscan/handlers.ts
@@ -0,0 +1,164 @@
+import type { PublicActions, WalletClient } from 'viem';
+import { base } from 'viem/chains';
+import { formatUnits, formatGwei } from 'viem';
+import type { z } from 'zod';
+import type { GetAddressTransactionsSchema } from './schemas.js';
+
+// Etherscan API endpoint for all supported chains
+const ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api';
+
+// Helper function to handle Etherscan API requests using V2 API
+async function makeEtherscanRequest(
+ params: Record,
+): Promise {
+ // Add API key if available
+ const apiKey = process.env.ETHERSCAN_API_KEY;
+ if (apiKey) {
+ params.apikey = apiKey;
+ } else {
+ throw new Error('ETHERSCAN_API_KEY is not set');
+ }
+
+ // Build query string
+ const queryParams = new URLSearchParams();
+ Object.entries(params).forEach(([key, value]) => {
+ queryParams.append(key, value);
+ });
+
+ try {
+ const response = await fetch(`${ETHERSCAN_API_URL}?${queryParams.toString()}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ // Handle Etherscan API errors
+ if (data.status === '0' && data.message === 'NOTOK') {
+ throw new Error(`Etherscan API error: ${data.result}`);
+ }
+
+ return data;
+ } catch (error) {
+ throw new Error(`Failed to fetch from Etherscan API: ${error instanceof Error ? error.message : String(error)}`);
+ }
+}
+
+export async function getAddressTransactionsHandler(
+ wallet: WalletClient & PublicActions,
+ args: z.infer,
+): Promise {
+ // Get chain ID from args or wallet
+ const chainId = args.chainId ?? wallet.chain?.id ?? base.id;
+
+ // Request parameters for normal transactions
+ const txParams: Record = {
+ chainid: chainId.toString(),
+ module: 'account',
+ action: 'txlist',
+ address: args.address,
+ startblock: (args.startblock ?? 0).toString(),
+ endblock: (args.endblock ?? "latest").toString(),
+ page: (args.page ?? 1).toString(),
+ offset: (args.offset ?? 5).toString(),
+ sort: args.sort ?? 'desc',
+ };
+
+ // API call to get 'normal' transaction data
+ const txData = await makeEtherscanRequest(txParams);
+
+ // Get ERC20 token transfers data within block range and map to transaction hash
+ const tokenTransfersByHash: Record = {};
+ if (txData.status === '1' && Array.isArray(txData.result) && txData.result.length > 0) {
+
+ // Find min and max block numbers based on sort order
+ const blockNumbers = txData.result.map((tx: any) => parseInt(tx.blockNumber));
+
+ let minBlock: number;
+ let maxBlock: number;
+ if (args.sort === 'asc') {
+ minBlock = blockNumbers[0];
+ maxBlock = blockNumbers[blockNumbers.length - 1];
+ } else {
+ minBlock = blockNumbers[blockNumbers.length - 1];
+ maxBlock = blockNumbers[0];
+ }
+
+ // Request parameters for ERC20 token transfers
+ const tokenTxParams: Record = {
+ chainid: chainId.toString(),
+ module: 'account',
+ action: 'tokentx',
+ address: args.address,
+ startblock: (minBlock-1).toString(),
+ endblock: (maxBlock+1).toString(),
+ page: '1',
+ offset: '100',
+ sort: args.sort ?? 'desc',
+ };
+
+ // API call to get ERC20 token transfer data
+ const tokenTxData = await makeEtherscanRequest(tokenTxParams);
+
+ if (tokenTxData.status === '1' && Array.isArray(tokenTxData.result)) {
+
+ // Map token transfers that match transaction hashes
+ const txHashes = new Set(txData.result.map((tx: any) => tx.hash));
+
+ tokenTxData.result.forEach((tokenTx: any) => {
+ if (txHashes.has(tokenTx.hash)) {
+ if (!tokenTransfersByHash[tokenTx.hash]) {
+ tokenTransfersByHash[tokenTx.hash] = [];
+ }
+
+ tokenTransfersByHash[tokenTx.hash].push({
+ from: tokenTx.from,
+ contractAddress: tokenTx.contractAddress,
+ to: tokenTx.to,
+ value: formatUnits(BigInt(tokenTx.value), tokenTx.tokenDecimal) + ' ' + tokenTx.tokenSymbol,
+ tokenName: tokenTx.tokenName,
+ });
+ }
+ });
+ }
+ }
+
+ // Format the transaction data
+ if (txData.status === '1' && Array.isArray(txData.result)) {
+ const filteredResults = txData.result.map((tx: any) => {
+ // Convert Unix timestamp to human-readable date
+ const date = new Date(parseInt(tx.timeStamp) * 1000);
+ const formattedDate = date.toISOString();
+
+ // Calculate paid fee in ETH
+ const feeWei = BigInt(tx.gasUsed) * BigInt(tx.gasPrice);
+ const feeInEth = formatUnits(feeWei, 18);
+
+ const result = {
+ timeStamp: formattedDate + ' UTC',
+ hash: tx.hash,
+ nonce: tx.nonce,
+ from: tx.from,
+ to: tx.to,
+ value: formatUnits(BigInt(tx.value), 18) + ' ETH',
+ gasPrice: formatGwei(BigInt(tx.gasPrice)) + ' gwei',
+ isError: tx.isError,
+ txreceipt_status: tx.txreceipt_status,
+ input: tx.input,
+ contractAddress: tx.contractAddress,
+ feeInEth: feeInEth + ' ETH',
+ methodId: tx.methodId,
+ functionName: tx.functionName,
+ tokenTransfers: tokenTransfersByHash[tx.hash] || []
+ };
+
+ return result;
+ });
+
+ // Add debug information to the response
+ return filteredResults;
+ }
+
+ return txData;
+}
diff --git a/src/tools/etherscan/index.ts b/src/tools/etherscan/index.ts
new file mode 100644
index 0000000..a5f6c7c
--- /dev/null
+++ b/src/tools/etherscan/index.ts
@@ -0,0 +1,10 @@
+import { generateTool } from '../../utils.js';
+import { getAddressTransactionsHandler } from './handlers.js';
+import { GetAddressTransactionsSchema } from './schemas.js';
+
+export const getAddressTransactionsTool = generateTool({
+ name: 'etherscan_address_transactions',
+ description: 'Gets a list of transactions for an address using Etherscan API',
+ inputSchema: GetAddressTransactionsSchema,
+ toolHandler: getAddressTransactionsHandler,
+});
diff --git a/src/tools/etherscan/schemas.ts b/src/tools/etherscan/schemas.ts
new file mode 100644
index 0000000..08d6058
--- /dev/null
+++ b/src/tools/etherscan/schemas.ts
@@ -0,0 +1,11 @@
+import { z } from 'zod';
+
+export const GetAddressTransactionsSchema = z.object({
+ address: z.string().describe('The address to get transactions for'),
+ startblock: z.number().optional().describe('Starting block number (defaults to 0)'),
+ endblock: z.number().optional().describe('Ending block number (defaults to 99999999)'),
+ page: z.number().min(1).optional().describe('Page number (defaults to 1)'),
+ offset: z.number().min(1).max(1000).optional().describe('Number of transactions per page (1-1000, defaults to 5)'),
+ sort: z.enum(['asc', 'desc']).optional().describe('Sort transactions by block number (asc or desc, defaults to desc)'),
+ chainId: z.number().optional().describe('The chain ID (defaults to chain the wallet is connected to)'),
+});
diff --git a/src/tools/index.ts b/src/tools/index.ts
index b254d32..3720a25 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -1,5 +1,6 @@
import { callContractTool } from './contracts/index.js';
import { erc20BalanceTool, erc20TransferTool } from './erc20/index.js';
+import { getAddressTransactionsTool } from './etherscan/index.js';
import { getMorphoVaultsTool } from './morpho/index.js';
import { getOnrampAssetsTool, onrampTool } from './onramp/index.js';
import { buyOpenRouterCreditsTool } from './open-router/index.js';
@@ -13,6 +14,7 @@ export const baseMcpTools: ToolWithHandler[] = [
erc20BalanceTool,
erc20TransferTool,
buyOpenRouterCreditsTool,
+ getAddressTransactionsTool,
];
export const toolToHandler: Record = baseMcpTools.reduce<
From 4dd5a05a6cbe7a9d97df0fc50776b20dba0db3c6 Mon Sep 17 00:00:00 2001
From: Philippe d'Argent
Date: Fri, 21 Mar 2025 16:13:04 +0100
Subject: [PATCH 2/2] add tool to get contract info from etherscan
---
README.md | 22 ++++
src/tools/etherscan/handlers.ts | 193 +++++++++++++++++++++++++-------
src/tools/etherscan/index.ts | 17 ++-
src/tools/etherscan/schemas.ts | 37 +++++-
src/tools/index.ts | 6 +-
5 files changed, 226 insertions(+), 49 deletions(-)
diff --git a/README.md b/README.md
index f489b59..f2099a5 100644
--- a/README.md
+++ b/README.md
@@ -352,6 +352,28 @@ Example query to Claude:
> "Show me the most recent transactions for address 0xc5102fE9359FD9a28f877a67E36B0F050d81a3CC."
+### etherscan_contract_info
+
+Gets detailed information about a smart contract using Etherscan API.
+
+Parameters:
+
+- `address`: The contract address to get information for
+- `chainId`: The chain ID (defaults to chain the wallet is connected to)
+
+The tool returns the following information:
+- Contract name
+- Contract address
+- ABI
+- Contract creator address
+- Transaction hash where the contract was created
+- Creation timestamp
+- Current ETH balance of the contract
+
+Example query to Claude:
+
+> "Show me information about the contract at 0xc5102fE9359FD9a28f877a67E36B0F050d81a3CC."
+
## Security Considerations
- The configuration file contains sensitive information (API keys and seed phrases). Ensure it's properly secured and not shared.
diff --git a/src/tools/etherscan/handlers.ts b/src/tools/etherscan/handlers.ts
index 6317606..aaea8c8 100644
--- a/src/tools/etherscan/handlers.ts
+++ b/src/tools/etherscan/handlers.ts
@@ -1,8 +1,12 @@
import type { PublicActions, WalletClient } from 'viem';
+import { formatGwei, formatUnits, isAddress } from 'viem';
+import { getBalance, getCode } from 'viem/actions';
import { base } from 'viem/chains';
-import { formatUnits, formatGwei } from 'viem';
import type { z } from 'zod';
-import type { GetAddressTransactionsSchema } from './schemas.js';
+import type {
+ GetAddressTransactionsSchema,
+ GetContractInfoSchema,
+} from './schemas.js';
// Etherscan API endpoint for all supported chains
const ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api';
@@ -10,7 +14,7 @@ const ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api';
// Helper function to handle Etherscan API requests using V2 API
async function makeEtherscanRequest(
params: Record,
-): Promise {
+): Promise> {
// Add API key if available
const apiKey = process.env.ETHERSCAN_API_KEY;
if (apiKey) {
@@ -18,7 +22,7 @@ async function makeEtherscanRequest(
} else {
throw new Error('ETHERSCAN_API_KEY is not set');
}
-
+
// Build query string
const queryParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
@@ -26,32 +30,41 @@ async function makeEtherscanRequest(
});
try {
- const response = await fetch(`${ETHERSCAN_API_URL}?${queryParams.toString()}`);
-
+ const response = await fetch(
+ `${ETHERSCAN_API_URL}?${queryParams.toString()}`,
+ );
+
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
-
+
const data = await response.json();
-
+
// Handle Etherscan API errors
if (data.status === '0' && data.message === 'NOTOK') {
throw new Error(`Etherscan API error: ${data.result}`);
}
-
+
return data;
} catch (error) {
- throw new Error(`Failed to fetch from Etherscan API: ${error instanceof Error ? error.message : String(error)}`);
+ throw new Error(
+ `Failed to fetch from Etherscan API: ${error instanceof Error ? error.message : String(error)}`,
+ );
}
}
export async function getAddressTransactionsHandler(
wallet: WalletClient & PublicActions,
args: z.infer,
-): Promise {
+): Promise {
// Get chain ID from args or wallet
const chainId = args.chainId ?? wallet.chain?.id ?? base.id;
-
+
+ // Validate address
+ if (!isAddress(args.address, { strict: false })) {
+ throw new Error(`Invalid address: ${args.address}`);
+ }
+
// Request parameters for normal transactions
const txParams: Record = {
chainid: chainId.toString(),
@@ -59,21 +72,29 @@ export async function getAddressTransactionsHandler(
action: 'txlist',
address: args.address,
startblock: (args.startblock ?? 0).toString(),
- endblock: (args.endblock ?? "latest").toString(),
+ endblock: (args.endblock ?? 'latest').toString(),
page: (args.page ?? 1).toString(),
offset: (args.offset ?? 5).toString(),
sort: args.sort ?? 'desc',
};
-
+
// API call to get 'normal' transaction data
const txData = await makeEtherscanRequest(txParams);
-
+
// Get ERC20 token transfers data within block range and map to transaction hash
- const tokenTransfersByHash: Record = {};
- if (txData.status === '1' && Array.isArray(txData.result) && txData.result.length > 0) {
-
+ const tokenTransfersByHash: Record<
+ string,
+ Array>
+ > = {};
+ if (
+ txData.status === '1' &&
+ Array.isArray(txData.result) &&
+ txData.result.length > 0
+ ) {
// Find min and max block numbers based on sort order
- const blockNumbers = txData.result.map((tx: any) => parseInt(tx.blockNumber));
+ const blockNumbers = txData.result.map((tx: Record) =>
+ parseInt(tx.blockNumber),
+ );
let minBlock: number;
let maxBlock: number;
@@ -84,57 +105,64 @@ export async function getAddressTransactionsHandler(
minBlock = blockNumbers[blockNumbers.length - 1];
maxBlock = blockNumbers[0];
}
-
+
// Request parameters for ERC20 token transfers
const tokenTxParams: Record = {
chainid: chainId.toString(),
module: 'account',
action: 'tokentx',
address: args.address,
- startblock: (minBlock-1).toString(),
- endblock: (maxBlock+1).toString(),
+ startblock: (minBlock - 1).toString(),
+ endblock: (maxBlock + 1).toString(),
page: '1',
- offset: '100',
+ offset: '100',
sort: args.sort ?? 'desc',
};
-
+
// API call to get ERC20 token transfer data
const tokenTxData = await makeEtherscanRequest(tokenTxParams);
-
+
if (tokenTxData.status === '1' && Array.isArray(tokenTxData.result)) {
-
// Map token transfers that match transaction hashes
- const txHashes = new Set(txData.result.map((tx: any) => tx.hash));
+ const txHashes = new Set(
+ txData.result.map((tx: Record) => tx.hash),
+ );
- tokenTxData.result.forEach((tokenTx: any) => {
+ tokenTxData.result.forEach((tokenTx: Record) => {
if (txHashes.has(tokenTx.hash)) {
if (!tokenTransfersByHash[tokenTx.hash]) {
tokenTransfersByHash[tokenTx.hash] = [];
}
-
+
tokenTransfersByHash[tokenTx.hash].push({
from: tokenTx.from,
contractAddress: tokenTx.contractAddress,
to: tokenTx.to,
- value: formatUnits(BigInt(tokenTx.value), tokenTx.tokenDecimal) + ' ' + tokenTx.tokenSymbol,
+ value:
+ formatUnits(
+ BigInt(tokenTx.value),
+ parseInt(tokenTx.tokenDecimal),
+ ) +
+ ' ' +
+ tokenTx.tokenSymbol,
tokenName: tokenTx.tokenName,
});
}
});
}
}
-
+
// Format the transaction data
if (txData.status === '1' && Array.isArray(txData.result)) {
- const filteredResults = txData.result.map((tx: any) => {
+ const filteredResults = txData.result.map((tx: Record) => {
// Convert Unix timestamp to human-readable date
const date = new Date(parseInt(tx.timeStamp) * 1000);
const formattedDate = date.toISOString();
-
- // Calculate paid fee in ETH
+
+ // Calculate paid fee in ETH
const feeWei = BigInt(tx.gasUsed) * BigInt(tx.gasPrice);
const feeInEth = formatUnits(feeWei, 18);
-
+
const result = {
timeStamp: formattedDate + ' UTC',
hash: tx.hash,
@@ -150,15 +178,98 @@ export async function getAddressTransactionsHandler(
feeInEth: feeInEth + ' ETH',
methodId: tx.methodId,
functionName: tx.functionName,
- tokenTransfers: tokenTransfersByHash[tx.hash] || []
+ tokenTransfers: tokenTransfersByHash[tx.hash] || [],
};
-
+
return result;
});
-
+
// Add debug information to the response
- return filteredResults;
+ return JSON.stringify(filteredResults);
+ }
+
+ return JSON.stringify(txData);
+}
+
+export async function getContractInfoHandler(
+ wallet: WalletClient & PublicActions,
+ args: z.infer,
+): Promise {
+ // Get chain ID from args or wallet
+ const chainId = args.chainId ?? wallet.chain?.id ?? base.id;
+
+ // Validate address
+ if (!isAddress(args.address, { strict: false })) {
+ throw new Error(`Invalid address: ${args.address}`);
+ }
+
+ // Check if address is a contract
+ const code = await getCode(wallet, { address: args.address });
+ if (code === '0x') {
+ throw new Error(`Address is not a contract: ${args.address}`);
+ }
+
+ // Get ETH balance of contract
+ const ethBalance = await getBalance(wallet, { address: args.address });
+
+ // Request parameters for contract source code
+ const sourceCodeParams: Record = {
+ chainid: chainId.toString(),
+ module: 'contract',
+ action: 'getsourcecode',
+ address: args.address,
+ };
+
+ // API call to get contract source code data
+ const sourceCodeData = await makeEtherscanRequest(sourceCodeParams);
+
+ // Request parameters for contract creation info
+ const creationParams: Record = {
+ chainid: chainId.toString(),
+ module: 'contract',
+ action: 'getcontractcreation',
+ contractaddresses: args.address,
+ };
+
+ // API call to get contract creation data
+ const creationData = await makeEtherscanRequest(creationParams);
+
+ // Extract and format the required information
+ const result = {
+ contractName: null as string | null,
+ contractAddress: args.address,
+ abi: null as string | null,
+ contractCreator: null as string | null,
+ txHash: null as string | null,
+ timestamp: null as string | null,
+ ethBalance: formatUnits(ethBalance, 18) + ' ETH',
+ };
+
+ if (
+ sourceCodeData.status === '1' &&
+ Array.isArray(sourceCodeData.result) &&
+ sourceCodeData.result.length > 0
+ ) {
+ const sourceCode = sourceCodeData.result[0];
+ result.abi = sourceCode.ABI;
+ result.contractName = sourceCode.ContractName;
+ }
+
+ if (
+ creationData.status === '1' &&
+ Array.isArray(creationData.result) &&
+ creationData.result.length > 0
+ ) {
+ const creation = creationData.result[0];
+ result.contractCreator = creation.contractCreator;
+ result.txHash = creation.txHash;
+
+ // Convert timestamp to human-readable date
+ if (creation.timestamp) {
+ const date = new Date(parseInt(creation.timestamp) * 1000);
+ result.timestamp = date.toISOString() + ' UTC';
+ }
}
-
- return txData;
+
+ return JSON.stringify(result);
}
diff --git a/src/tools/etherscan/index.ts b/src/tools/etherscan/index.ts
index a5f6c7c..e02f1c3 100644
--- a/src/tools/etherscan/index.ts
+++ b/src/tools/etherscan/index.ts
@@ -1,6 +1,12 @@
import { generateTool } from '../../utils.js';
-import { getAddressTransactionsHandler } from './handlers.js';
-import { GetAddressTransactionsSchema } from './schemas.js';
+import {
+ getAddressTransactionsHandler,
+ getContractInfoHandler,
+} from './handlers.js';
+import {
+ GetAddressTransactionsSchema,
+ GetContractInfoSchema,
+} from './schemas.js';
export const getAddressTransactionsTool = generateTool({
name: 'etherscan_address_transactions',
@@ -8,3 +14,10 @@ export const getAddressTransactionsTool = generateTool({
inputSchema: GetAddressTransactionsSchema,
toolHandler: getAddressTransactionsHandler,
});
+
+export const getContractInfoTool = generateTool({
+ name: 'etherscan_contract_info',
+ description: 'Gets contract information using Etherscan API',
+ inputSchema: GetContractInfoSchema,
+ toolHandler: getContractInfoHandler,
+});
diff --git a/src/tools/etherscan/schemas.ts b/src/tools/etherscan/schemas.ts
index 08d6058..28422b9 100644
--- a/src/tools/etherscan/schemas.ts
+++ b/src/tools/etherscan/schemas.ts
@@ -2,10 +2,37 @@ import { z } from 'zod';
export const GetAddressTransactionsSchema = z.object({
address: z.string().describe('The address to get transactions for'),
- startblock: z.number().optional().describe('Starting block number (defaults to 0)'),
- endblock: z.number().optional().describe('Ending block number (defaults to 99999999)'),
+ startblock: z
+ .number()
+ .optional()
+ .describe('Starting block number (defaults to 0)'),
+ endblock: z
+ .number()
+ .optional()
+ .describe('Ending block number (defaults to 99999999)'),
page: z.number().min(1).optional().describe('Page number (defaults to 1)'),
- offset: z.number().min(1).max(1000).optional().describe('Number of transactions per page (1-1000, defaults to 5)'),
- sort: z.enum(['asc', 'desc']).optional().describe('Sort transactions by block number (asc or desc, defaults to desc)'),
- chainId: z.number().optional().describe('The chain ID (defaults to chain the wallet is connected to)'),
+ offset: z
+ .number()
+ .min(1)
+ .max(1000)
+ .optional()
+ .describe('Number of transactions per page (1-1000, defaults to 5)'),
+ sort: z
+ .enum(['asc', 'desc'])
+ .optional()
+ .describe(
+ 'Sort transactions by block number (asc or desc, defaults to desc)',
+ ),
+ chainId: z
+ .number()
+ .optional()
+ .describe('The chain ID (defaults to chain the wallet is connected to)'),
+});
+
+export const GetContractInfoSchema = z.object({
+ address: z.string().describe('The contract address to get information for'),
+ chainId: z
+ .number()
+ .optional()
+ .describe('The chain ID (defaults to chain the wallet is connected to)'),
});
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 3720a25..1656bef 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -1,6 +1,9 @@
import { callContractTool } from './contracts/index.js';
import { erc20BalanceTool, erc20TransferTool } from './erc20/index.js';
-import { getAddressTransactionsTool } from './etherscan/index.js';
+import {
+ getAddressTransactionsTool,
+ getContractInfoTool,
+} from './etherscan/index.js';
import { getMorphoVaultsTool } from './morpho/index.js';
import { getOnrampAssetsTool, onrampTool } from './onramp/index.js';
import { buyOpenRouterCreditsTool } from './open-router/index.js';
@@ -15,6 +18,7 @@ export const baseMcpTools: ToolWithHandler[] = [
erc20TransferTool,
buyOpenRouterCreditsTool,
getAddressTransactionsTool,
+ getContractInfoTool,
];
export const toolToHandler: Record = baseMcpTools.reduce<