call_contract
Call whitelisted smart contracts on EVM and Solana. Provide contract address and transaction data to interact with on-chain protocols.
Instructions
Call a whitelisted smart contract. Requires CONTRACT_WHITELIST policy. For EVM: provide calldata (hex). For Solana: provide programId + instructionData + accounts.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| to | Yes | Contract address | |
| calldata | No | Hex-encoded calldata (EVM) | |
| abi | No | ABI fragment for decoding (EVM) | |
| value | No | Native token value in smallest units (wei). Example: "1000000000000000000" = 1 ETH | |
| programId | No | Program ID (Solana) | |
| instructionData | No | Base64-encoded instruction data (Solana) | |
| accounts | No | Account metas (Solana) | |
| network | No | Target network (e.g., "polygon-mainnet" or CAIP-2 "eip155:137"). Required for EVM wallets; auto-resolved for Solana. | |
| wallet_id | No | Target wallet ID. Required for multi-wallet sessions; auto-resolved when session has a single wallet. | |
| gas_condition | No | Gas price condition for deferred execution. At least one of max_gas_price or max_priority_fee required. |
Implementation Reference
- Primary handler: registers call_contract tool on the MCP server. Builds a CONTRACT_CALL request body with optional EVM (to, calldata, abi, value) and Solana (programId, instructionData, accounts) parameters, plus network, wallet_id, and gas_condition. Sends POST /v1/transactions/send and returns the result.
export function registerCallContract(server: McpServer, apiClient: ApiClient, walletContext?: WalletContext): void { server.tool( 'call_contract', withWalletPrefix('Call a whitelisted smart contract. Requires CONTRACT_WHITELIST policy. For EVM: provide calldata (hex). For Solana: provide programId + instructionData + accounts.', walletContext?.walletName), { to: z.string().describe('Contract address'), calldata: z.string().optional().describe('Hex-encoded calldata (EVM)'), abi: z.array(z.record(z.string(), z.unknown())).optional().describe('ABI fragment for decoding (EVM)'), value: z.string().optional().describe('Native token value in smallest units (wei). Example: "1000000000000000000" = 1 ETH'), programId: z.string().optional().describe('Program ID (Solana)'), instructionData: z.string().optional().describe('Base64-encoded instruction data (Solana)'), accounts: z.array(z.object({ pubkey: z.string(), isSigner: z.boolean(), isWritable: z.boolean(), })).optional().describe('Account metas (Solana)'), network: z.string().optional().describe('Target network (e.g., "polygon-mainnet" or CAIP-2 "eip155:137"). Required for EVM wallets; auto-resolved for Solana.'), wallet_id: z.string().optional().describe('Target wallet ID. Required for multi-wallet sessions; auto-resolved when session has a single wallet.'), gas_condition: z.object({ max_gas_price: z.string().optional().describe('Max gas price in wei (EVM baseFee+priorityFee)'), max_priority_fee: z.string().optional().describe('Max priority fee in wei (EVM) or micro-lamports (Solana)'), timeout: z.number().optional().describe('Max wait time in seconds (60-86400)'), }).optional().describe('Gas price condition for deferred execution. At least one of max_gas_price or max_priority_fee required.'), }, async (args) => { const body: Record<string, unknown> = { type: 'CONTRACT_CALL', to: args.to }; if (args.calldata !== undefined) body.calldata = args.calldata; if (args.abi !== undefined) body.abi = args.abi; if (args.value !== undefined) body.value = args.value; if (args.programId !== undefined) body.programId = args.programId; if (args.instructionData !== undefined) body.instructionData = args.instructionData; if (args.accounts !== undefined) body.accounts = args.accounts; if (args.network !== undefined) body.network = args.network; if (args.wallet_id) body.walletId = args.wallet_id; if (args.gas_condition) { body.gasCondition = { maxGasPrice: args.gas_condition.max_gas_price, maxPriorityFee: args.gas_condition.max_priority_fee, timeout: args.gas_condition.timeout, }; } const result = await apiClient.post('/v1/transactions/send', body); return toToolResult(result); }, ); } - Input schema using Zod: defines all parameters including to (required), calldata, abi, value for EVM, and programId, instructionData, accounts for Solana, plus network, wallet_id, and gas_condition.
{ to: z.string().describe('Contract address'), calldata: z.string().optional().describe('Hex-encoded calldata (EVM)'), abi: z.array(z.record(z.string(), z.unknown())).optional().describe('ABI fragment for decoding (EVM)'), value: z.string().optional().describe('Native token value in smallest units (wei). Example: "1000000000000000000" = 1 ETH'), programId: z.string().optional().describe('Program ID (Solana)'), instructionData: z.string().optional().describe('Base64-encoded instruction data (Solana)'), accounts: z.array(z.object({ pubkey: z.string(), isSigner: z.boolean(), isWritable: z.boolean(), })).optional().describe('Account metas (Solana)'), network: z.string().optional().describe('Target network (e.g., "polygon-mainnet" or CAIP-2 "eip155:137"). Required for EVM wallets; auto-resolved for Solana.'), wallet_id: z.string().optional().describe('Target wallet ID. Required for multi-wallet sessions; auto-resolved when session has a single wallet.'), gas_condition: z.object({ max_gas_price: z.string().optional().describe('Max gas price in wei (EVM baseFee+priorityFee)'), max_priority_fee: z.string().optional().describe('Max priority fee in wei (EVM) or micro-lamports (Solana)'), timeout: z.number().optional().describe('Max wait time in seconds (60-86400)'), }).optional().describe('Gas price condition for deferred execution. At least one of max_gas_price or max_priority_fee required.'), }, - packages/mcp/src/tools/call-contract.ts:14-59 (registration)Registration: exports registerCallContract which calls server.tool('call_contract', ...) to register the tool with the MCP server.
export function registerCallContract(server: McpServer, apiClient: ApiClient, walletContext?: WalletContext): void { server.tool( 'call_contract', withWalletPrefix('Call a whitelisted smart contract. Requires CONTRACT_WHITELIST policy. For EVM: provide calldata (hex). For Solana: provide programId + instructionData + accounts.', walletContext?.walletName), { to: z.string().describe('Contract address'), calldata: z.string().optional().describe('Hex-encoded calldata (EVM)'), abi: z.array(z.record(z.string(), z.unknown())).optional().describe('ABI fragment for decoding (EVM)'), value: z.string().optional().describe('Native token value in smallest units (wei). Example: "1000000000000000000" = 1 ETH'), programId: z.string().optional().describe('Program ID (Solana)'), instructionData: z.string().optional().describe('Base64-encoded instruction data (Solana)'), accounts: z.array(z.object({ pubkey: z.string(), isSigner: z.boolean(), isWritable: z.boolean(), })).optional().describe('Account metas (Solana)'), network: z.string().optional().describe('Target network (e.g., "polygon-mainnet" or CAIP-2 "eip155:137"). Required for EVM wallets; auto-resolved for Solana.'), wallet_id: z.string().optional().describe('Target wallet ID. Required for multi-wallet sessions; auto-resolved when session has a single wallet.'), gas_condition: z.object({ max_gas_price: z.string().optional().describe('Max gas price in wei (EVM baseFee+priorityFee)'), max_priority_fee: z.string().optional().describe('Max priority fee in wei (EVM) or micro-lamports (Solana)'), timeout: z.number().optional().describe('Max wait time in seconds (60-86400)'), }).optional().describe('Gas price condition for deferred execution. At least one of max_gas_price or max_priority_fee required.'), }, async (args) => { const body: Record<string, unknown> = { type: 'CONTRACT_CALL', to: args.to }; if (args.calldata !== undefined) body.calldata = args.calldata; if (args.abi !== undefined) body.abi = args.abi; if (args.value !== undefined) body.value = args.value; if (args.programId !== undefined) body.programId = args.programId; if (args.instructionData !== undefined) body.instructionData = args.instructionData; if (args.accounts !== undefined) body.accounts = args.accounts; if (args.network !== undefined) body.network = args.network; if (args.wallet_id) body.walletId = args.wallet_id; if (args.gas_condition) { body.gasCondition = { maxGasPrice: args.gas_condition.max_gas_price, maxPriorityFee: args.gas_condition.max_priority_fee, timeout: args.gas_condition.timeout, }; } const result = await apiClient.post('/v1/transactions/send', body); return toToolResult(result); }, ); } - packages/mcp/src/server.ts:100-101 (registration)Registration call site: imports and invokes registerCallContract from server.ts to wire the tool into the MCP server factory.
registerCallContract(server, apiClient, walletContext); registerApproveToken(server, apiClient, walletContext); - Alternative registration in the openclaw-plugin package: registers call_contract tool with input schema and handler via PluginApi.registerTool. Same CONTRACT_CALL logic posting to /v1/transactions/send.
// Tool 12: call_contract api.registerTool({ name: 'call_contract', description: 'Call a whitelisted smart contract. Requires CONTRACT_WHITELIST policy. For EVM: provide calldata (hex). For Solana: provide programId + instructionData + accounts.', inputSchema: { type: 'object', properties: { to: { type: 'string', description: 'Contract address' }, calldata: { type: 'string', description: 'Hex-encoded calldata (EVM)' }, value: { type: 'string', description: 'Native token value in smallest units (wei).' }, programId: { type: 'string', description: 'Program ID (Solana)' }, instructionData: { type: 'string', description: 'Base64-encoded instruction data (Solana)' }, accounts: { type: 'array', description: 'Account metas (Solana)', items: { type: 'object', properties: { pubkey: { type: 'string' }, isSigner: { type: 'boolean' }, isWritable: { type: 'boolean' }, }, }, }, network: { type: 'string', description: 'Target network.' }, wallet_id: { type: 'string', description: 'Target wallet ID.' }, gas_condition: { type: 'object', properties: { max_gas_price: { type: 'string' }, max_priority_fee: { type: 'string' }, timeout: { type: 'number' }, }, }, }, required: ['to'], }, handler: async (args) => { const body: Record<string, unknown> = { type: 'CONTRACT_CALL', to: args['to'] }; if (args['calldata'] !== undefined) body['calldata'] = args['calldata']; if (args['value'] !== undefined) body['value'] = args['value']; if (args['programId'] !== undefined) body['programId'] = args['programId']; if (args['instructionData'] !== undefined) body['instructionData'] = args['instructionData']; if (args['accounts'] !== undefined) body['accounts'] = args['accounts']; if (args['network'] !== undefined) body['network'] = args['network']; if (args['wallet_id']) body['walletId'] = args['wallet_id']; if (args['gas_condition']) { const gc = args['gas_condition'] as Record<string, unknown>; body['gasCondition'] = { maxGasPrice: gc['max_gas_price'], maxPriorityFee: gc['max_priority_fee'], timeout: gc['timeout'] }; } const result = await client.post('/v1/transactions/send', body); return toResult(result); },