Skip to main content
Glama
callContractFunction.ts8.83 kB
import { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js"; import { getNetwork } from "../shared/account.js"; import { getDefaultProvider, Wallet, Contract } from "@coti-io/coti-ethers"; import { ERC20_ABI, ERC721_ABI } from "../constants/abis.js"; import { z } from "zod"; export const CALL_CONTRACT_FUNCTION: ToolAnnotations = { title: "Call Contract Function", name: "call_contract_function", description: "Call any function on a smart contract on the COTI blockchain. " + "Supports both read-only (view/pure) functions and state-changing (write) functions. " + "Read-only functions return data without creating a transaction. " + "State-changing functions create transactions and may require gas_limit. " + "Returns the function result in a human-readable format.", inputSchema: { private_key: z.string().describe("Private key of the account (tracked by AI from previous operations)"), aes_key: z.string().optional().describe("AES key for private transactions (tracked by AI). Required for private operations."), network: z.enum(['testnet', 'mainnet']).describe("Network to use: 'testnet' or 'mainnet' (required)."), contract_address: z.string().describe("Address of the smart contract to call"), function_name: z.string().describe("Name of the function to call on the contract"), function_args: z.array(z.string()).describe("Array of arguments to pass to the function (can be empty if function takes no arguments)"), abi: z.string().optional().describe("Optional JSON string representation of the contract ABI. If not provided, will attempt to use standard ERC20/ERC721 ABIs."), gas_limit: z.string().optional().describe("Optional gas limit for state-changing functions. Not needed for read-only functions."), }, }; /** * Checks if the provided arguments are valid for the call_contract_function tool. * @param args The arguments to check. * @returns True if the arguments are valid, false otherwise. */ export function isCallContractFunctionArgs(args: unknown): args is { contract_address: string, function_name: string, function_args: string[], abi?: string , private_key?: string, aes_key?: string, network: 'testnet' | 'mainnet', gas_limit?: string } { return ( typeof args === "object" && args !== null && "contract_address" in args && typeof (args as { contract_address: string }).contract_address === "string" && "function_name" in args && typeof (args as { function_name: string }).function_name === "string" && "function_args" in args && Array.isArray((args as { function_args: string[] }).function_args) && (!("gas_limit" in args) || typeof (args as { gas_limit: string }).gas_limit === "string") ); } /** * Calls a function on a smart contract. * @param contract_address The address of the smart contract to call. * @param function_name The name of the function to call. * @param function_args The arguments to pass to the function. * @param abi The ABI of the smart contract. * @param gas_limit Optional gas limit for state-changing functions. * @returns An object with function call results and formatted text. */ export async function performCallContractFunction(private_key: string, aes_key: string, contract_address: string, function_name: string, function_args: string[], network: 'testnet' | 'mainnet', abi?: string, gas_limit?: string): Promise<{ contractAddress: string, functionName: string, functionArgs: any[], result: any, formattedResult: string, contractType?: string, formattedText: string }> { try { const provider = getDefaultProvider(getNetwork(network)); const wallet = new Wallet(private_key, provider); wallet.setAesKey(aes_key); let contractAbi; if (abi) { try { contractAbi = JSON.parse(abi); } catch (e) { throw new Error(`Invalid ABI format: ${e instanceof Error ? e.message : String(e)}`); } } else { try { const tempContract = new Contract(contract_address, ERC20_ABI, wallet); await tempContract.decimals(); contractAbi = ERC20_ABI; } catch (e) { try { const tempContract = new Contract(contract_address, ERC721_ABI, wallet); await tempContract.ownerOf(1); contractAbi = ERC721_ABI; } catch (e2) { throw new Error('Could not determine contract type. Please provide the ABI.'); } } } const contract = new Contract(contract_address, contractAbi, wallet); if (!contract[function_name]) { throw new Error(`Function '${function_name}' not found in contract. Check the function name or provide a custom ABI.`); } const processedArgs = function_args.map(arg => { if (arg.toLowerCase() === 'true') return true; if (arg.toLowerCase() === 'false') return false; if (arg.match(/^0x[0-9a-fA-F]{40}$/)) return arg; if (arg.match(/^-?\d+$/)) return BigInt(arg); if (arg.match(/^-?\d+\.\d+$/)) return parseFloat(arg); return arg; }); // Add gas limit for state-changing transactions if provided const txOptions: any = gas_limit ? { gasLimit: gas_limit } : {}; const result = await contract[function_name](...processedArgs, txOptions); // Define replacer at function scope for reuse with BigInt serialization const replacer = (key: string, value: any) => { if (typeof value === 'bigint') { return value.toString(); } return value; }; let formattedResult: string; if (typeof result === 'object' && result !== null) { if (Array.isArray(result)) { formattedResult = JSON.stringify(result, replacer, 2); } else if (result._isBigNumber || typeof result.toString === 'function') { formattedResult = result.toString(); } else { formattedResult = JSON.stringify(result, replacer, 2); } } else if (typeof result === 'bigint') { formattedResult = result.toString(); } else { formattedResult = String(result); } const formattedText = `Function Call Successful!\n\nContract: ${contract_address}\n\nFunction: ${function_name}\n\nArguments: ${JSON.stringify(processedArgs, replacer)}\n\nResult: ${formattedResult}`; let contractType: string | undefined; if (contractAbi === ERC20_ABI) { contractType = 'ERC20'; } else if (contractAbi === ERC721_ABI) { contractType = 'ERC721'; } // Convert BigInt in processedArgs to string for safe serialization const serializableArgs = processedArgs.map(arg => typeof arg === 'bigint' ? arg.toString() : arg ); return { contractAddress: contract_address, functionName: function_name, functionArgs: serializableArgs, result, formattedResult, contractType, formattedText }; } catch (error) { console.error('Error calling contract function:', error); throw new Error(`Failed to call contract function: ${error instanceof Error ? error.message : String(error)}`); } } /** * Handler for the callContractFunction tool * @param args The arguments for the tool * @returns The tool response */ export async function callContractFunctionHandler(args: any): Promise<any> { if (!isCallContractFunctionArgs(args)) { throw new Error("Invalid arguments for call_contract_function"); } const { contract_address, function_name, function_args, abi, network, private_key, aes_key, gas_limit } = args; if (!private_key || !aes_key) { throw new Error("private_key and aes_key are required"); } const results = await performCallContractFunction(private_key, aes_key, contract_address, function_name, function_args, network, abi, gas_limit); return { structuredContent: { contractAddress: results.contractAddress, functionName: results.functionName, functionArgs: results.functionArgs, result: results.result, formattedResult: results.formattedResult, contractType: results.contractType }, content: [{ type: "text", text: results.formattedText }], isError: false, }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/davibauer/coti-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server