← blog
Protocol · Security

Why EIP-712 attestations beat MCP’s trust model.

Yoshi · 2026-04-17

The Model Context Protocol has a security model that assumes the server is honest. The server hands the agent a tool description, the agent reads that description, and the agent acts. There is no signature on the description, no signature on the result, and no way to prove after the fact that the server returned what it claimed to return. When the server is compromised — whether through CVE-2026-39313, the systemic Anthropic SDK flaw, or old-fashioned supply-chain attack — the agent has no basis to refuse.

MeterCall modules take the opposite position. Every request carries a signed envelope. Every response carries a signed receipt. Every cross-chain attestation is signed by a rotatable hot key that a verifier contract checks on-chain. If the server lies, the signature breaks. If the signature breaks, the call is void. The agent does not have to trust the server; it has to verify the receipt.

The structural difference

MCP trust is advisory. A server says “here is my tool transfer_funds and here is its schema,” and the agent decides whether to believe it. If the description is rewritten between initial discovery and actual invocation, the agent has no way to notice. This is the root cause of roughly every MCP disclosure this year.

MeterCall trust is cryptographic. A module commits to an EIP-712 domain, a request typehash, and a result typehash. The request is signed by the caller’s session key. The result is signed by the module’s hot key, which is in turn anchored to a cold attestation on-chain. A man-in-the-middle who rewrites the description, the request, or the result breaks the signature and the receipt verifier rejects.

The typehashes

We anchor three objects. The call request, the call result, and the bridge attestation.

// Call request, signed by the caller session key
bytes32 constant CALL_REQUEST_TYPEHASH = keccak256(
  "CallRequest(bytes32 moduleId,bytes32 methodId,bytes32 argsHash,uint256 nonce,uint64 notAfter)"
);

// Call result, signed by the module hot key
bytes32 constant CALL_RESULT_TYPEHASH = keccak256(
  "CallResult(bytes32 requestHash,bytes32 resultHash,uint256 latencyMs,uint64 signedAt)"
);

// Bridge attestation, signed by the bridge signer
bytes32 constant BRIDGE_ATTEST_TYPEHASH = keccak256(
  "BridgeAttest(bytes32 srcTxHash,uint32 srcChainId,uint32 dstChainId,address recipient,address token,uint256 amount,uint64 attestedAt)"
);

These three typehashes cover virtually every useful operation in the L4 router. A module call is CallRequest in, CallResult out. A cross-chain withdrawal is BridgeAttest against the bridge contract. That is the whole threat surface for the signer path.

Why EIP-712 specifically

Three reasons. One, EIP-712 is widely implemented, widely audited, and supported by every hardware wallet and every Solidity toolchain. We do not need to invent a signing format and attract the associated footguns. Two, EIP-712 domain separation makes replay across chains, across contracts, and across versions structurally impossible, which matters a lot when the router fronts 30-plus chains. Three, EIP-712 signatures are verifiable on-chain cheaply, which means slashing conditions for dishonest signers are easy to enforce.

MCP has none of these properties because MCP has no signature primitive at all. The protocol simply does not address the question.

The verifier

function verifyResult(
  CallRequest calldata req,
  CallResult calldata res,
  bytes calldata sigModule
) external view returns (bool) {
  bytes32 reqHash = _hashRequest(req);
  require(res.requestHash == reqHash, "mismatch");
  bytes32 digest = _toTypedDataHash(_hashResult(res));
  address signer = ECDSA.recover(digest, sigModule);
  return moduleHotKey[req.moduleId] == signer;
}

Every call the agent makes returns (result, sigModule). The agent or any downstream contract can call verifyResult before acting on the result. If the signer is not the currently registered hot key for that module, or the request hash does not match, the call is treated as if it never happened. Settlement is gated on verification, so a forged result cannot pull funds.

MCP answers “what tools exist?” MeterCall answers “what tools exist, who signed the last call, and can I slash them if they lied?” Those are different protocols.

Key rotation without downtime

The module hot key is rotatable. A module’s cold attestation on-chain declares a public key and a version. When we rotate — routinely every thirty days, or immediately on any suspicion — the new key is attested, the old key has a grace window where both are valid, then the old key is revoked. Receipts signed in the grace window are still verifiable after rotation because the verifier looks up the key version from the receipt, not from the current state. This is the same pattern Circle wishes they had before the $232M CCTP bridge incident.

What this buys the agent

What the agent still has to do

Signatures are necessary, not sufficient. The agent still has to decide whether the signed result is semantically acceptable. A signed result that says “I transferred to 0xdead” is still a transfer to 0xdead. This is why Sniper sits in front of every outbound call, and why the Shield policy layer can veto even a well-signed result if the destination is flagged. Cryptography answers “did the module say this?” Policy answers “should we do it?”

Migration path for MCP servers

If you run an MCP server today, you can adopt the envelope without forking. Wrap your tool invocations in a thin signer that emits a CallResult alongside the existing MCP response. Agents that are MeterCall-aware will verify and trust. Agents that are MCP-only will ignore the extra field and fall back to the unauthenticated path. There is no reason not to ship the signature now and flip the policy to “require signed” once your callers have upgraded.

We published a reference signer in the SDKs directory. About forty lines of TypeScript and a rotatable keystore.

CVE-2026-39313 — TheHackerWire
Anthropic MCP supply-chain flaw — ITPro
Timeline of MCP security breaches — Authzed
EIP-712 specification — ethereum.org