Add Receipt Required to an MCP server in 10 minutes.
No receipt, no irreversible action. Publish an Action Control Manifest, wrap one dangerous tool, and return 428 Receipt Required until the agent brings an EP-RECEIPT-v1bound to the exact action. Valid receipt runs. Same receipt again is replay-refused. Forged receipt never reaches the mutation.
Turn a declared tool list into a bounded integration.
The first command is a dry run. After you review its classifications, the second creates a manifest, production wrapper, and synthetic local refusal check. The local check proves only that its handler was not called. Production still requires a durable provenance ledger, a shared atomic consumption store, pinned keys, and a wrapper on every path to the real credential.
# inspect first; this makes no change npx @emilia-protocol/scan protect ./tools.json # create the reviewed manifest, wrapper, and local refusal check npx @emilia-protocol/scan protect ./tools.json --apply node emilia/verify-setup.mjs
Publish /.well-known/agent-action-control.json.
Start with one irreversible MCP tool. The manifest is deliberately boring: it names the tool, the exact fields and CAID selector the receipt must bind, the assurance class, and—only for profiles your approval service can actually complete—the pinned acquisition endpoint.
{
"@version": "EP-ACTION-CONTROL-MANIFEST-v0.2",
"profile": "agent-action-control",
"service": {
"name": "Acme MCP",
"issuer": "https://mcp.acme.com",
"manifest_url": "https://mcp.acme.com/.well-known/agent-action-control.json"
},
"defaults": {
"decision_point": "pre_effect_commit",
"missing_receipt": "refuse",
"invalid_receipt": "refuse",
"stale_receipt": "refuse",
"replay": "one_time_consumption",
"evidence_log": "strict"
},
"evidence_profiles": {
"authorization_receipt": "EP-RECEIPT-v1",
"execution_attestation": "EP-EXECUTION-ATTESTATION-v1",
"reliance_packet": "EP-RELIANCE-PACKET-v1"
},
"actions": [
{
"id": "mcp.release_payment",
"match": { "protocol": "mcp", "tool": "release_payment" },
"action_type": "payment.release",
"risk": "high",
"receipt_required": true,
"assurance_class": "class_a",
"max_age_sec": 900,
"control": {
"enforcement_point": "pre_effect_commit",
"status": 428,
"challenge_header": "Receipt-Required",
"proof_header": "X-EMILIA-Receipt",
"authorization": {
"authorization_endpoint": "https://approve.example.com/api/v1/approvals",
"flow": "EP-APPROVAL-v1"
},
"authorization_receipt": {
"required": true,
"profile": "EP-RECEIPT-v1",
"verifier": "offline"
},
"replay": { "mode": "one_time_consumption", "receipt_id_required": true },
"execution_binding": {
"required": true,
"source": "system_of_record",
"required_fields": [
"action_type", "amount", "currency", "beneficiary_account_hash", "action_caid"
],
"caid_selector": { "field": "action_caid" }
},
"evidence_output": {
"audit_event": true,
"execution_attestation": true,
"reliance_packet": true,
"blocked_attempts": true
}
},
"conformance": { "level": "EG-1", "checks": ["execution_drift_refused"] }
}
]
}Wrap the tool dispatcher.
Install @emilia-protocol/require-receipt. Resolve the tool requirement from the manifest, verify the receipt offline, consume it before the write, then call the real tool handler.
import {
createGate,
findActionControl,
} from '@emilia-protocol/gate';
const manifest = await fetch('https://mcp.acme.com/.well-known/agent-action-control.json')
.then((r) => r.json());
const approverKeys = JSON.parse(process.env.EMILIA_APPROVER_KEYS_JSON);
const allowedOrigins = process.env.EMILIA_ALLOWED_ORIGINS.split(',');
// The store is durable, ownership-fenced, and permanent. reserve() is atomic;
// an uncertain reservation remains closed until reconciliation.
const gate = createGate({
manifest,
trustedKeys: [process.env.EMILIA_ISSUER_PUBKEY].filter(Boolean),
approverKeys,
rpId: process.env.EMILIA_RP_ID,
allowedOrigins,
quorumPolicies: PINNED_QUORUM_POLICIES,
store: productionReceiptStore,
});
function stripEpControlArgs(args = {}) {
const { __ep, emilia_receipt, ...clean } = args;
return clean;
}
export async function guardedCallTool(name, args, extra = {}) {
const req = findActionControl(manifest, { protocol: 'mcp', tool: name });
if (!req?.receipt_required) return handleTool(name, args, extra);
const receipt = args.__ep?.receipt || args.emilia_receipt || extra._meta?.emilia_receipt;
const clean = stripEpControlArgs(args);
const observedAction = readExactActionFromSystemOfRecord(req.action_type, clean);
const result = await gate.run({
selector: { protocol: 'mcp', tool: name },
receipt,
observedAction,
}, () => handleTool(name, clean, extra));
return result.ok ? result.result : result.body;
}Turn a refusal into a machine-completable approval.
The agent sends the exact challenged action to an endpoint it already trusts, receives a human-review URL plus a separate poll capability, waits for a terminal Class-A decision, then retries. The manifest never supplies the requester credential and never becomes a trust root.
import {
beginReceiptApproval,
pollReceiptApproval,
} from '@emilia-protocol/require-receipt';
const pending = await beginReceiptApproval({
authorization: challenge.required.authorization,
trustedAuthorization: configuredApprovalEndpoint,
challenge: challenge.required,
action: exactSystemOfRecordAction,
approver_id: 'approver@example.com',
idempotency_key: crypto.randomUUID(),
requesterAuthorization: () => `Bearer ${process.env.EMILIA_API_KEY}`,
});
const terminal = await pollReceiptApproval({
authorization: challenge.required.authorization,
trustedAuthorization: configuredApprovalEndpoint,
request_id: pending.request_id,
poll_token: pending.poll_token,
});
if (terminal.status !== 'approved') throw new Error(terminal.status);
await retryOriginalCall({ receipt: terminal.receipt });Run it cold, no account, no API key.
The repo ships three manifest-driven MCP examples: payment release, repo deletion, and production deploy. They exercise the real verifier and the replay check.
FAST=1 node examples/mcp/payment-server.mjs FAST=1 node examples/mcp/github-admin.mjs FAST=1 node examples/mcp/prod-deploy.mjs # Each demo proves: # 1. no receipt -> 428 Receipt Required # 2. exact signed receipt -> tool runs # 3. same receipt again -> replay_refused # 4. tampered receipt -> refused
Use the system-of-record guard when the write is real.
For production, withMcpReceiptGuarddrives the v1 flow: require receipt, request signoff if needed, consume before mutation, run the tool, then emit execution evidence. If consume fails, your handler is never called.
import { EPClient } from '@emilia-protocol/sdk';
import { withMcpReceiptGuard } from '@emilia-protocol/mcp-guard';
const ep = new EPClient({ apiKey }); // EP_API_KEY from your env
const guardedHandleTool = withMcpReceiptGuard(handleTool, {
client: ep,
executingSystem: 'acme-mcp-server',
annotations: {
release_payment: {
irreversible: true,
actionType: 'payment.release',
targetResourceId: (args) => args.payment_id,
amount: (args) => args.amount,
currency: (args) => args.currency,
approverId: 'ap_controller_jane',
onSignoffRequired: async ({ signoff }) => waitForApprovedSignoff(signoff.signoff_id),
},
},
});
// dispatch through guardedHandleTool instead of handleToolSame rail for ordinary APIs.
If the dangerous action is an HTTP route instead of an MCP tool, opt into the same 428 rail with one middleware. Omit statusCodeonly when you deliberately need legacy 402/x402 compatibility.
import { requireEmiliaReceipt } from '@emilia-protocol/require-receipt';
app.post(
'/release-payment',
requireEmiliaReceipt({
trustedKeys: [process.env.EMILIA_ISSUER_PUBKEY],
action: 'payment.release',
statusCode: 428,
manifestUrl: '/.well-known/agent-action-control.json',
authorization: {
authorization_endpoint: 'https://approve.example.com/api/v1/approvals',
flow: 'EP-APPROVAL-v1',
},
requiredFields: ['action_type', 'amount', 'currency', 'beneficiary_account_hash', 'action_caid'],
caidSelector: { field: 'action_caid' },
maxAgeSec: 900,
}),
(req, res) => res.json({ released: true, receipt: req.emiliaReceipt.receipt_id }),
);428 is the clean precondition rail: bring an authorization receipt before mutation. 402 remains available for x402/AP2-compatible flows, but new Receipt Required integrations should advertise 428.
No. The manifest is a refusal contract. It tells agents which action needs proof; the receipt verifier, authority checks, quorum rules, and consume-before-write gate decide whether the action may run.
No for the offline demand demo: pin issuer keys and verify locally. For production issuance, signoff, one-time consumption, and execution attestation, use the SDK or your EMILIA host.
One dangerous MCP tool that returns 428 without proof, runs with an exact signed receipt, refuses replay, and refuses a forged receipt. That four-step ritual is the adoption wedge.
Receipt Required is not identity, permissions, or a correctness oracle. It is a fail-closed refusal rail: if an agent changes money, code, permissions, records, or regulated state, the system can demand portable proof of exactly who authorized exactly what under exactly which policy.