Skip to content

Commit dd3c354

Browse files
committed
feat: add multicall support for batch contract reads
Add multicall tool to batch multiple contract read calls into a single RPC request using the Multicall3 contract. This significantly reduces latency and RPC usage when querying multiple contract functions. Features: - Automatic ABI fetching from block explorers for verified contracts - Fallback to common function signatures (ERC20 standards) - Partial failure support - returns successful results even if some calls fail - ENS name support for contract addresses - Works across all 60+ supported networks New tool: - multicall: Batch multiple contract reads in a single call Use cases: - Portfolio analysis: Get multiple token balances at once - Price aggregation: Query multiple DEX prices simultaneously - Token metadata: Fetch name, symbol, decimals, totalSupply in one call - Protocol state: Check multiple contract parameters efficiently Service functions: - Added multicall() to src/core/services/contracts.ts Documentation: - Updated tool count from 24 to 25 - Added multicall usage example with USDC contract - Added multicall to API reference table
1 parent 8591ac2 commit dd3c354

3 files changed

Lines changed: 199 additions & 10 deletions

File tree

README.md

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -475,11 +475,50 @@ console.log(result);
475475
// }
476476
```
477477

478+
### Example: Batch Multiple Calls with Multicall
479+
480+
```javascript
481+
// Example of using multicall to batch multiple contract reads in a single RPC call
482+
const mcp = new McpClient("http://localhost:3000");
483+
484+
const result = await mcp.invokeTool("multicall", {
485+
network: "ethereum",
486+
calls: [
487+
{
488+
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
489+
functionName: "balanceOf",
490+
args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
491+
},
492+
{
493+
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
494+
functionName: "symbol",
495+
},
496+
{
497+
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
498+
functionName: "decimals",
499+
},
500+
],
501+
});
502+
503+
console.log(result);
504+
// {
505+
// network: "ethereum",
506+
// totalCalls: 3,
507+
// successfulCalls: 3,
508+
// failedCalls: 0,
509+
// results: [
510+
// { contractAddress: "0xA0b...", functionName: "balanceOf", result: "1000000000", status: "success" },
511+
// { contractAddress: "0xA0b...", functionName: "symbol", result: "USDC", status: "success" },
512+
// { contractAddress: "0xA0b...", functionName: "decimals", result: "6", status: "success" }
513+
// ]
514+
// }
515+
```
516+
478517
## 📚 API Reference
479518

480519
### Tools
481520

482-
The server provides 24 focused MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.**
521+
The server provides 25 focused MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.**
483522

484523
#### Wallet Information
485524

@@ -522,11 +561,12 @@ The server provides 24 focused MCP tools for agents. **All tools that accept add
522561

523562
#### Smart Contract Interactions
524563

525-
| Tool Name | Description | Key Parameters |
526-
| ------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
527-
| `get_contract_abi` | Fetch contract ABI from block explorer (60+ networks) | `contractAddress` (address/ENS), `network` |
528-
| `read_contract` | Read smart contract state (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `abiJson` (optional), `network` |
529-
| `write_contract` | Execute state-changing functions (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `value` (optional), `abiJson` (optional), `network` |
564+
| Tool Name | Description | Key Parameters |
565+
| ------------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
566+
| `get_contract_abi` | Fetch contract ABI from block explorer (60+ networks) | `contractAddress` (address/ENS), `network` |
567+
| `read_contract` | Read smart contract state (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `abiJson` (optional), `network` |
568+
| `write_contract` | Execute state-changing functions (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `value` (optional), `abiJson` (optional), `network` |
569+
| `multicall` | Batch multiple read calls into a single RPC request (uses Multicall3) | `calls[]` (array of contract calls), `allowFailure` (optional), `network` |
530570

531571
#### Token Transfers
532572

src/core/services/contracts.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {
2-
type Address,
3-
type Hash,
1+
import {
2+
type Address,
3+
type Hash,
44
type Hex,
55
type ReadContractParameters,
66
type GetLogsParameters,
@@ -46,8 +46,33 @@ export async function getLogs(params: GetLogsParameters, network = 'ethereum'):
4646
export async function isContract(addressOrEns: string, network = 'ethereum'): Promise<boolean> {
4747
// Resolve ENS name to address if needed
4848
const address = await resolveAddress(addressOrEns, network);
49-
49+
5050
const client = getPublicClient(network);
5151
const code = await client.getBytecode({ address });
5252
return code !== undefined && code !== '0x';
53+
}
54+
55+
/**
56+
* Batch multiple contract read calls into a single RPC request using Multicall3
57+
* @param contracts Array of contract calls to batch
58+
* @param allowFailure If true, returns partial results even if some calls fail
59+
* @param network Network name or chain ID
60+
* @returns Array of results with status
61+
*/
62+
export async function multicall(
63+
contracts: Array<{
64+
address: Address;
65+
abi: any[];
66+
functionName: string;
67+
args?: any[];
68+
}>,
69+
allowFailure = true,
70+
network = 'ethereum'
71+
): Promise<any> {
72+
const client = getPublicClient(network);
73+
74+
return await client.multicall({
75+
contracts: contracts as any,
76+
allowFailure
77+
});
5378
}

src/core/tools.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,130 @@ export function registerEVMTools(server: McpServer) {
863863
}
864864
);
865865

866+
server.registerTool(
867+
"multicall",
868+
{
869+
description: "Batch multiple contract read calls into a single RPC request. Significantly reduces latency and RPC usage when querying multiple functions. Uses the Multicall3 contract deployed on all major networks. Perfect for portfolio analysis, price aggregation, and querying multiple contract states efficiently.",
870+
inputSchema: {
871+
calls: z.array(z.object({
872+
contractAddress: z.string().describe("The contract address"),
873+
functionName: z.string().describe("Function name to call"),
874+
args: z.array(z.string()).optional().describe("Function arguments as strings"),
875+
abiJson: z.string().optional().describe("Contract ABI as JSON string (optional - will auto-fetch if not provided)")
876+
})).describe("Array of contract calls to batch together"),
877+
allowFailure: z.boolean().optional().describe("If true, returns partial results even if some calls fail. Defaults to true."),
878+
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
879+
},
880+
annotations: {
881+
title: "Multicall (Batch Read)",
882+
readOnlyHint: true,
883+
destructiveHint: false,
884+
idempotentHint: true,
885+
openWorldHint: true
886+
}
887+
},
888+
async ({ calls, allowFailure = true, network = "ethereum" }) => {
889+
try {
890+
// Build contracts array with ABIs
891+
const contractsWithAbis = await Promise.all(
892+
calls.map(async (call) => {
893+
let abi: any[];
894+
let functionAbi: any;
895+
896+
// If ABI is provided, use it
897+
if (call.abiJson) {
898+
try {
899+
abi = services.parseABI(call.abiJson);
900+
functionAbi = services.getFunctionFromABI(abi, call.functionName);
901+
} catch (error) {
902+
throw new Error(`Error parsing ABI for ${call.contractAddress}: ${error instanceof Error ? error.message : String(error)}`);
903+
}
904+
} else {
905+
// Try to auto-fetch ABI
906+
try {
907+
const fetchedAbi = await services.fetchContractABI(call.contractAddress as Address, network);
908+
abi = services.parseABI(fetchedAbi);
909+
functionAbi = services.getFunctionFromABI(abi, call.functionName);
910+
} catch (fetchError) {
911+
// Fall back to common function signatures
912+
const commonFunctions: { [key: string]: any } = {
913+
'name': { inputs: [], outputs: [{ type: 'string' }], stateMutability: 'view' },
914+
'symbol': { inputs: [], outputs: [{ type: 'string' }], stateMutability: 'view' },
915+
'decimals': { inputs: [], outputs: [{ type: 'uint8' }], stateMutability: 'view' },
916+
'totalSupply': { inputs: [], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
917+
'balanceOf': { inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
918+
'allowance': { inputs: [{ type: 'address' }, { type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
919+
};
920+
921+
if (!commonFunctions[call.functionName]) {
922+
throw new Error(`Could not auto-fetch ABI for ${call.contractAddress}. Function '${call.functionName}' not in common signatures. Please provide abiJson parameter.`);
923+
}
924+
925+
functionAbi = {
926+
name: call.functionName,
927+
type: 'function',
928+
inputs: commonFunctions[call.functionName].inputs,
929+
outputs: commonFunctions[call.functionName].outputs,
930+
stateMutability: 'view'
931+
};
932+
}
933+
}
934+
935+
return {
936+
address: call.contractAddress as Address,
937+
abi: [functionAbi],
938+
functionName: call.functionName,
939+
args: call.args || []
940+
};
941+
})
942+
);
943+
944+
// Execute multicall
945+
const results = await services.multicall(contractsWithAbis, allowFailure, network);
946+
947+
// Format results
948+
const formattedResults = results.map((result: any, index: number) => {
949+
const call = calls[index];
950+
if (result.status === 'success') {
951+
return {
952+
contractAddress: call.contractAddress,
953+
functionName: call.functionName,
954+
args: call.args,
955+
result: result.result?.toString(),
956+
status: 'success'
957+
};
958+
} else {
959+
return {
960+
contractAddress: call.contractAddress,
961+
functionName: call.functionName,
962+
args: call.args,
963+
error: result.error?.message || 'Unknown error',
964+
status: 'failure'
965+
};
966+
}
967+
});
968+
969+
return {
970+
content: [{
971+
type: "text",
972+
text: JSON.stringify({
973+
network,
974+
totalCalls: calls.length,
975+
successfulCalls: formattedResults.filter((r: any) => r.status === 'success').length,
976+
failedCalls: formattedResults.filter((r: any) => r.status === 'failure').length,
977+
results: formattedResults
978+
}, null, 2)
979+
}]
980+
};
981+
} catch (error) {
982+
return {
983+
content: [{ type: "text", text: `Error executing multicall: ${error instanceof Error ? error.message : String(error)}` }],
984+
isError: true
985+
};
986+
}
987+
}
988+
);
989+
866990
// ============================================================================
867991
// TRANSFER TOOLS (Write operations)
868992
// ============================================================================

0 commit comments

Comments
 (0)