Skip to content

Commit 8cee62f

Browse files
committed
feat: add write_contract tool and interact_with_contract prompt for safe contract interaction.
1 parent 7fdc5c2 commit 8cee62f

4 files changed

Lines changed: 298 additions & 10 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
![MCP](https://img.shields.io/badge/MCP-1.22.0+-blue)
77
![Viem](https://img.shields.io/badge/Viem-2.39.3+-green)
88

9-
A comprehensive Model Context Protocol (MCP) server that provides blockchain services across 60+ EVM-compatible networks. This server enables AI agents to interact with Ethereum, Optimism, Arbitrum, Base, Polygon, and many other EVM chains with a unified interface through 21 tools and 9 AI-guided prompts.
9+
A comprehensive Model Context Protocol (MCP) server that provides blockchain services across 60+ EVM-compatible networks. This server enables AI agents to interact with Ethereum, Optimism, Arbitrum, Base, Polygon, and many other EVM chains with a unified interface through 22 tools and 10 AI-guided prompts.
1010

1111
## 📋 Contents
1212

@@ -75,7 +75,7 @@ All services are exposed through a consistent interface of MCP tools, resources,
7575
### Smart Contract Interactions
7676

7777
- **Read contract state** through view/pure functions
78-
- **Write services** with private key signing
78+
- **Write to contracts** - Execute any state-changing function with automatic ABI fetching
7979
- **Contract verification** to distinguish from EOAs
8080
- **Event logs** retrieval and filtering
8181
- **Automatic ABI fetching** from Etherscan v2 API across all 60+ networks (no need to know ABIs in advance)
@@ -93,6 +93,7 @@ All services are exposed through a consistent interface of MCP tools, resources,
9393
- **Transaction preparation** - Guidance for planning and executing transfers
9494
- **Wallet analysis** - Tools for analyzing wallet activity and holdings
9595
- **Smart contract exploration** - Interactive ABI fetching and contract analysis
96+
- **Contract interaction** - Safe execution of write operations on smart contracts
9697
- **Network information** - Learning about EVM networks and comparisons
9798
- **Approval auditing** - Reviewing and managing token approvals
9899
- **Error diagnosis** - Troubleshooting transaction failures
@@ -440,7 +441,7 @@ console.log(result);
440441

441442
### Tools
442443

443-
The server provides 21 focused MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.**
444+
The server provides 22 focused MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.**
444445

445446
#### Wallet Information
446447

@@ -487,6 +488,7 @@ The server provides 21 focused MCP tools for agents. **All tools that accept add
487488
|-----------|-------------|----------------|
488489
| `get_contract_abi` | Fetch contract ABI from block explorer (60+ networks) | `contractAddress` (address/ENS), `network` |
489490
| `read_contract` | Read smart contract state (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `abiJson` (optional), `network` |
491+
| `write_contract` | Execute state-changing functions (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `value` (optional), `abiJson` (optional), `network` |
490492

491493
#### Token Transfers
492494

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"module": "src/index.ts",
44
"type": "module",
55
"version": "2.0.0",
6-
"description": "MCP server for interacting with EVM-compatible blockchains - supports 21 tools and 9 prompts across 60+ networks",
6+
"description": "MCP server for interacting with EVM-compatible blockchains - supports 22 tools and 10 prompts across 60+ networks",
77
"bin": {
88
"evm-mcp-server": "./bin/cli.js"
99
},

src/core/prompts.ts

Lines changed: 179 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ Before executing any transfer:
4646
1. **Wallet Verification**: Call \`get_wallet_address\` to confirm the sending wallet
4747
2. **Balance Check**:
4848
${tokenType === "native"
49-
? "- Call `get_balance` to verify native token balance"
50-
: "- Call `get_token_balance` with tokenAddress=${tokenAddress} to verify balance"}
49+
? "- Call `get_balance` to verify native token balance"
50+
: "- Call `get_token_balance` with tokenAddress=${tokenAddress} to verify balance"}
5151
3. **Gas Analysis**: Call \`get_gas_price\` to assess current network costs
5252
${tokenType === "erc20" ? `4. **Approval Check**: Call \`get_allowance\` to verify approval (if needed for protocols)` : ""}
5353
@@ -204,8 +204,8 @@ Provide structured diagnosis:
204204
205205
### 3. Token Balances
206206
${tokenList.length > 0
207-
? `- Call \`get_token_balance\` for each token:\n${tokenList.map(t => ` * ${t}`).join('\n')}`
208-
: `- If specific tokens provided: call \`get_token_balance\` for each
207+
? `- Call \`get_token_balance\` for each token:\n${tokenList.map(t => ` * ${t}`).join('\n')}`
208+
: `- If specific tokens provided: call \`get_token_balance\` for each
209209
- Include token symbol and decimals if available`}
210210
211211
## Output Format
@@ -475,13 +475,13 @@ Look for:
475475
## Exploration Strategy
476476
477477
${fetchAbi === 'true'
478-
? `### With Full ABI (Fetched)
478+
? `### With Full ABI (Fetched)
479479
1. Call \`get_contract_abi\` to fetch verified ABI
480480
2. Parse all available functions
481481
3. Call \`read_contract\` for important state functions
482482
4. Build comprehensive understanding
483483
`
484-
: `### Without Full ABI (Probing)
484+
: `### Without Full ABI (Probing)
485485
1. Test common function signatures
486486
2. Call \`read_contract\` with standard functions:
487487
- name(), symbol(), decimals(), totalSupply()
@@ -567,6 +567,179 @@ For each contract type:
567567
// NETWORK & EDUCATION PROMPTS
568568
// ============================================================================
569569

570+
server.registerPrompt(
571+
"interact_with_contract",
572+
{
573+
description: "Safely execute write operations on a smart contract with validation and confirmation",
574+
argsSchema: {
575+
contractAddress: z.string().describe("Contract address to interact with"),
576+
functionName: z.string().describe("Function to call (e.g., 'mint', 'swap', 'stake')"),
577+
args: z.string().optional().describe("Comma-separated function arguments"),
578+
value: z.string().optional().describe("ETH value to send (for payable functions)"),
579+
network: z.string().optional().describe("Network name (default: ethereum)")
580+
}
581+
},
582+
({ contractAddress, functionName, args, value, network = "ethereum" }) => {
583+
const argsList = args ? args.split(',').map(a => a.trim()) : [];
584+
return {
585+
messages: [{
586+
role: "user",
587+
content: {
588+
type: "text",
589+
text: `# Smart Contract Interaction
590+
591+
**Objective**: Safely execute ${functionName} on contract ${contractAddress} on ${network}
592+
593+
## Prerequisites Check
594+
595+
### 1. Wallet Verification
596+
- Call \`get_wallet_address\` to confirm the wallet that will execute this transaction
597+
- Verify this is the correct wallet for this operation
598+
599+
### 2. Contract Analysis
600+
- Call \`get_contract_abi\` to fetch and analyze the contract ABI
601+
- Verify the function exists and understand its parameters
602+
- Check function type:
603+
* **View/Pure**: Read-only (use \`read_contract\` instead)
604+
* **Nonpayable**: State-changing, no ETH required
605+
* **Payable**: State-changing, can accept ETH
606+
607+
### 3. Function Parameter Validation
608+
For function: **${functionName}**
609+
${argsList.length > 0 ? `Arguments provided: ${argsList.join(', ')}` : 'No arguments provided'}
610+
611+
- Verify parameter types match the ABI
612+
- Validate addresses are checksummed
613+
- Check numeric values are in correct units
614+
- Resolve any ENS names to addresses if needed
615+
616+
### 4. Pre-execution Checks
617+
618+
**Balance Check**:
619+
- Call \`get_balance\` to verify sufficient native token balance
620+
- Account for gas costs + value (if payable)
621+
622+
**Gas Estimation**:
623+
- Call \`get_gas_price\` to estimate transaction cost
624+
- Calculate total cost: (gas_price * estimated_gas) + value
625+
626+
**State Verification** (if applicable):
627+
- Use \`read_contract\` to check current contract state
628+
- Verify conditions are met (e.g., allowances, balances, ownership)
629+
630+
## Execution Process
631+
632+
### 1. Present Summary to User
633+
Before executing, show:
634+
- **Contract**: ${contractAddress}
635+
- **Network**: ${network}
636+
- **Function**: ${functionName}
637+
- **Arguments**: ${argsList.length > 0 ? argsList.join(', ') : 'None'}
638+
${value ? `- **Value**: ${value} ETH` : ''}
639+
- **From**: [wallet address from step 1]
640+
- **Estimated Gas Cost**: [from gas estimation]
641+
- **Total Cost**: [gas + value]
642+
643+
### 2. Request User Confirmation
644+
⚠️ **IMPORTANT**: Always ask user to confirm before executing write operations
645+
- Clearly state what will happen
646+
- Show all costs involved
647+
- Explain any risks or irreversible actions
648+
649+
### 3. Execute Transaction
650+
Only after user confirms:
651+
\`\`\`
652+
Call write_contract with:
653+
- contractAddress: "${contractAddress}"
654+
- functionName: "${functionName}"
655+
${argsList.length > 0 ? `- args: ${JSON.stringify(argsList)}` : ''}
656+
${value ? `- value: "${value}"` : ''}
657+
- network: "${network}"
658+
\`\`\`
659+
660+
### 4. Monitor Transaction
661+
After execution:
662+
1. Return transaction hash to user
663+
2. Call \`wait_for_transaction\` to monitor confirmation
664+
3. Call \`get_transaction_receipt\` to verify success
665+
4. If failed, call \`diagnose_transaction\` to understand why
666+
667+
## Output Format
668+
669+
**Pre-Execution Summary**:
670+
- Contract details
671+
- Function and parameters
672+
- Cost breakdown
673+
- Risk assessment
674+
675+
**Confirmation Request**:
676+
"Ready to execute ${functionName} on ${contractAddress}. This will cost approximately [X] ETH. Proceed? (yes/no)"
677+
678+
**Execution Result**:
679+
- Transaction Hash: [hash]
680+
- Status: Pending/Confirmed/Failed
681+
- Block Number: [if confirmed]
682+
- Gas Used: [actual gas used]
683+
- Total Cost: [final cost]
684+
685+
## Safety Considerations
686+
687+
### Critical Checks
688+
- ✅ Verify contract is verified on block explorer
689+
- ✅ Check function parameters are correct type and format
690+
- ✅ Ensure sufficient balance for gas + value
691+
- ✅ Validate addresses (no typos, correct network)
692+
- ✅ Understand what the function does before calling
693+
694+
### Common Risks
695+
- **Irreversible**: Most blockchain transactions cannot be undone
696+
- **Gas Loss**: Failed transactions still consume gas
697+
- **Approval Risks**: Be careful with unlimited approvals
698+
- **Reentrancy**: Some functions may be vulnerable
699+
- **Access Control**: Verify you have permission to call this function
700+
701+
### Red Flags
702+
🚨 Stop and warn user if:
703+
- Contract is not verified
704+
- Function requires admin/owner privileges you don't have
705+
- Unusually high gas estimate
706+
- Suspicious parameter values
707+
- Contract has known vulnerabilities
708+
709+
## Error Handling
710+
711+
If transaction fails:
712+
1. Get the revert reason from receipt
713+
2. Check common issues:
714+
- Insufficient balance/allowance
715+
- Access control (onlyOwner, etc.)
716+
- Invalid parameters
717+
- Contract paused
718+
- Slippage (for DEX operations)
719+
3. Provide actionable fix suggestions
720+
4. Offer to retry with corrected parameters
721+
722+
## Example Workflow
723+
724+
For a token mint operation:
725+
1. ✅ Verify wallet
726+
2. ✅ Fetch contract ABI
727+
3. ✅ Check mint function exists and is callable
728+
4. ✅ Verify sufficient ETH for gas
729+
5. ✅ Show summary: "Minting 1 NFT will cost ~0.002 ETH"
730+
6. ⏸️ Wait for user confirmation
731+
7. ✅ Execute write_contract
732+
8. ✅ Monitor transaction
733+
9. ✅ Confirm success and return token ID
734+
735+
**Remember**: Always prioritize user safety and transparency!
736+
`
737+
}
738+
}]
739+
};
740+
}
741+
);
742+
570743
server.registerPrompt(
571744
"explain_evm_concept",
572745
{

src/core/tools.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,119 @@ export function registerEVMTools(server: McpServer) {
765765
}
766766
);
767767

768+
server.registerTool(
769+
"write_contract",
770+
{
771+
description: "Execute state-changing functions on a smart contract. Automatically fetches ABI from block explorer if not provided (requires ETHERSCAN_API_KEY). Use this to call any write function on verified contracts. Requires EVM_PRIVATE_KEY to be configured.",
772+
inputSchema: {
773+
contractAddress: z.string().describe("The contract address"),
774+
functionName: z.string().describe("Function name to call (e.g., 'mint', 'swap', 'stake', 'approve')"),
775+
args: z.array(z.string()).optional().describe("Function arguments as strings (e.g., ['0xAddress', '1000000'])"),
776+
value: z.string().optional().describe("ETH value to send with transaction in ether (e.g., '0.1' for payable functions)"),
777+
abiJson: z.string().optional().describe("Full contract ABI as JSON string (optional - will auto-fetch verified contract ABI if not provided)"),
778+
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
779+
},
780+
annotations: {
781+
title: "Write to Smart Contract",
782+
readOnlyHint: false,
783+
destructiveHint: true,
784+
idempotentHint: false,
785+
openWorldHint: true
786+
}
787+
},
788+
async ({ contractAddress, functionName, args = [], value, abiJson, network = "ethereum" }) => {
789+
try {
790+
const privateKey = getConfiguredPrivateKey();
791+
const senderAddress = getWalletAddressFromKey();
792+
const client = await services.getPublicClient(network);
793+
794+
let abi: any[] | undefined;
795+
let functionAbi: any;
796+
797+
// If ABI is provided, use it
798+
if (abiJson) {
799+
try {
800+
abi = services.parseABI(abiJson);
801+
functionAbi = services.getFunctionFromABI(abi, functionName);
802+
} catch (error) {
803+
return {
804+
content: [{
805+
type: "text",
806+
text: `Error parsing provided ABI: ${error instanceof Error ? error.message : String(error)}`
807+
}],
808+
isError: true
809+
};
810+
}
811+
} else {
812+
// Try to auto-fetch ABI from block explorer
813+
try {
814+
const fetchedAbi = await services.fetchContractABI(contractAddress as Address, network);
815+
abi = services.parseABI(fetchedAbi);
816+
functionAbi = services.getFunctionFromABI(abi, functionName);
817+
} catch (fetchError) {
818+
return {
819+
content: [{
820+
type: "text",
821+
text: `Error: Could not auto-fetch ABI (${fetchError instanceof Error ? fetchError.message : String(fetchError)}). Please provide the contract ABI using the abiJson parameter, or use get_contract_abi to fetch it first.`
822+
}],
823+
isError: true
824+
};
825+
}
826+
}
827+
828+
// Validate that this is not a view/pure function
829+
if (functionAbi.stateMutability === 'view' || functionAbi.stateMutability === 'pure') {
830+
return {
831+
content: [{
832+
type: "text",
833+
text: `Error: Function '${functionName}' is a ${functionAbi.stateMutability} function and cannot modify state. Use read_contract instead.`
834+
}],
835+
isError: true
836+
};
837+
}
838+
839+
// Prepare write parameters
840+
const writeParams: any = {
841+
address: contractAddress as Address,
842+
abi: [functionAbi],
843+
functionName: functionName,
844+
args: args as any
845+
};
846+
847+
// Add value if provided (for payable functions)
848+
if (value) {
849+
const { parseEther } = await import('viem');
850+
writeParams.value = parseEther(value);
851+
}
852+
853+
// Execute the write operation
854+
const txHash = await services.writeContract(privateKey, writeParams, network);
855+
856+
return {
857+
content: [{
858+
type: "text",
859+
text: JSON.stringify({
860+
network,
861+
contractAddress,
862+
function: functionName,
863+
args: args.length > 0 ? args : undefined,
864+
value: value || undefined,
865+
from: senderAddress,
866+
txHash,
867+
abiSource: abiJson ? 'provided' : 'auto-fetched',
868+
message: "Transaction sent. Use get_transaction_receipt or wait_for_transaction to check confirmation."
869+
}, null, 2)
870+
}]
871+
};
872+
} catch (error) {
873+
return {
874+
content: [{ type: "text", text: `Error writing to contract: ${error instanceof Error ? error.message : String(error)}` }],
875+
isError: true
876+
};
877+
}
878+
}
879+
);
880+
768881
// ============================================================================
769882
// TRANSFER TOOLS (Write operations)
770883
// ============================================================================

0 commit comments

Comments
 (0)