dev.send
Sign and broadcast unsigned transactions from write tools directly onchain using a local private key. Intended for development testing.
Instructions
DEV ONLY — Sign and broadcast an unsigned transaction using a local private key (PK env var). For production, use a dedicated wallet MCP server (Fireblocks, Safe, Turnkey, etc.) instead of this tool. Takes the transaction object returned by any write.* tool and submits it onchain.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| to | Yes | Target contract address | |
| data | Yes | Encoded calldata (hex) | |
| value | No | Value in wei (default '0') | 0 |
| chain_id | No | Chain ID: 8453 (Base), 130 (Unichain), or 10 (Optimism) |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| signer | Yes | ||
| txHash | Yes | ||
| status | Yes | ||
| blockNumber | Yes | ||
| gasLimit | Yes | ||
| gasUsed | Yes |
Implementation Reference
- src/tools/dev/send.ts:20-125 (handler)The main handler function for the 'dev.send' tool. It registers the tool with server.registerTool("dev.send", ...), defines inputSchema (to, data, value, chain_id), fetches the private key from env PK, validates inputs, estimates gas, sends the transaction via viem WalletClient, waits for receipt, and returns structured output (signer, txHash, status, blockNumber, gasLimit, gasUsed).
export function registerSendTool(server: McpServer, chains: Record<ChainId, ChainConfig>) { server.registerTool( "dev.send", { annotations: { title: "Sign and Send Transaction", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, }, description: "DEV ONLY — Sign and broadcast an unsigned transaction using a local private key (PK env var). For production, use a dedicated wallet MCP server (Fireblocks, Safe, Turnkey, etc.) instead of this tool. Takes the transaction object returned by any write.* tool and submits it onchain.", outputSchema: DevSendOutput, inputSchema: { to: z.string().describe("Target contract address"), data: z.string().describe("Encoded calldata (hex)"), value: z.string().default("0").describe("Value in wei (default '0')"), chain_id: z.number().default(8453).describe(CHAIN_ID_DESCRIPTION), }, }, async (params) => { try { const pk = process.env.PK; if (!pk) { return { content: [ { type: "text" as const, text: "Error: PK not set. Either create a .env file with PK=0x... in the server directory, or set PK in your MCP client config env block. This tool is for development only — use a dedicated wallet MCP server for production.", }, ], isError: true, }; } const validTo = validateAddress(params.to, "to"); const calldata = validateHexCalldata(params.data, "data"); const valueWei = parseWeiDecimalString(params.value, "value"); const chain = VIEM_CHAINS[params.chain_id]; const chainConfig = chains[params.chain_id as ChainId]; if (!chainConfig) { return { content: [ { type: "text" as const, text: `Error: Unsupported chain ID ${params.chain_id}` }, ], isError: true, }; } const transport = http(chainConfig.rpcUrl); const account = privateKeyToAccount(pk as `0x${string}`); const wallet = createWalletClient({ account, chain, transport }); const client = createPublicClient({ chain, transport }); const gasEstimate = await client.estimateGas({ account: account.address, to: validTo, data: calldata, value: valueWei, }); const gasLimit = (gasEstimate * 120n) / 100n; const hash = await wallet.sendTransaction({ to: validTo, data: calldata, value: valueWei, gas: gasLimit, chain, }); const receipt = await client.waitForTransactionReceipt({ hash, timeout: 60_000 }); const result = { signer: account.address, txHash: receipt.transactionHash, status: receipt.status, blockNumber: Number(receipt.blockNumber), gasLimit: Number(gasLimit), gasUsed: Number(receipt.gasUsed), }; return { content: [ { type: "text" as const, text: JSON.stringify(result, null, 2), }, ], structuredContent: result, }; } catch (err) { return { content: [ { type: "text" as const, text: `Error: ${err instanceof Error ? err.message : String(err)}`, }, ], isError: true, }; } }, ); } - src/tools/output-schemas.ts:70-77 (schema)The DevSendOutput Zod schema defines the output shape: signer (string), txHash (string), status (string), blockNumber (number), gasLimit (number), gasUsed (number). Used as the outputSchema for the tool.
export const DevSendOutput = z.object({ signer: z.string(), txHash: z.string(), status: z.string(), blockNumber: z.number(), gasLimit: z.number(), gasUsed: z.number(), }); - src/tools/dev/send.ts:34-39 (schema)Inline inputSchema for the tool: to (string), data (string, hex calldata), value (string, default '0'), chain_id (number, default 8453). Defined via Zod in the tool registration call.
inputSchema: { to: z.string().describe("Target contract address"), data: z.string().describe("Encoded calldata (hex)"), value: z.string().default("0").describe("Value in wei (default '0')"), chain_id: z.number().default(8453).describe(CHAIN_ID_DESCRIPTION), }, - src/tools/index.ts:26-26 (registration)Import of registerSendTool from './dev/send.js' in the central tool registration index.
import { registerSendTool } from "./dev/send.js"; - src/tools/index.ts:78-78 (registration)Invocation of registerSendTool(server, chains) to register the 'dev.send' tool.
registerSendTool(server, chains);