Wire Axiom into your MCP agent stack
Three integration scenarios, complete API reference, receipt schema, and security model. Written for engineers at regulated enterprises.
Architecture Overview
Axiom sits in the invocation path between your agent and its skill backend. Every skill call passes through the Axiom receipt middleware, which captures a cryptographic snapshot before and after execution.
The receipt captures four elements in a tamper-evident chain:
- inputs_hash — SHA-256 of skill inputs, computed before execution.
- output_hash — SHA-256 of skill output, computed after execution.
- signature — HMAC-SHA256 over the full receipt payload.
- timestamp — UTC time of invocation initiation.
The signing secret never leaves your infrastructure. Axiom operates in two modes: hosted (signing handled by Axiom's service) and self-hosted (signing secret deployed alongside your agent). The receipt ID is the only lookup key needed for independent verification.
Three Integration Scenarios
Scenario A — Standalone MCP Server
Run Axiom as a sidecar process alongside your MCP server. Axiom's MCP client proxies tool calls: invoke skill → capture receipt → return result + receipt. Minimal code change: point your agent at Axiom's MCP endpoint.
Setup
# Run the Axiom MCP test server alongside your own server node mcp-servers/axiom-test-server.js & node your-mcp-server.js
Wrapping an existing MCP server
// wrap-mcp-server.js
const { MCPClient } = require('./services/mcp');
// Point Axiom at your existing MCP server
const client = new MCPClient('node', ['./my-mcp-server.js']);
async function init() {
await client.connect();
// Every call through this client is now receipt-stamped
const { output, receipt } = await client.callTool(
'my-tool',
{ param: 'value' },
{ source: 'live', caller: 'my-agent-v1' }
);
console.log('Receipt ID:', receipt.id);
console.log('Verify:', `https://axiom-ai-19.polsia.app/verify?id=${receipt.id}`);
}
init();
callTool method handles input hashing, MCP communication, output hashing, signing, and DB persistence automatically. You get the receipt back alongside the tool result.
Scenario B — Claude Agent Harness (Claude Code, Claude CLI)
Wrap skill invocations in the Claude agent's tool-use layer. Before calling the tool: compute input hash, store pending receipt. After receiving response: compute output hash, sign receipt, persist.
Implementation pattern (Node.js)
// axiom-tool-wrapper.js
const { hashInputs, hashOutput, signReceipt } = require('./services/crypto');
const { createReceipt } = require('./db/receipts');
// Call this BEFORE every tool invocation
async function beforeSkillCall(skillName, inputs, caller) {
const timestamp = new Date().toISOString();
const inputsHash = hashInputs({ skill: skillName, args: inputs });
return { timestamp, inputsHash }; // store pending state
}
// Call this AFTER every tool invocation
async function afterSkillCall(beforeState, skillName, output, caller) {
const { timestamp, inputsHash } = beforeState;
const outputHash = hashOutput(output);
const signature = signReceipt(skillName, timestamp, inputsHash, outputHash);
const receipt = await createReceipt({
skill_name: skillName,
timestamp,
inputs_hash: inputsHash,
output_hash: outputHash,
signature,
verification_status: 'verified',
source: 'live',
caller: caller || null,
});
return receipt;
}
// ── Example harness integration ──────────────────────────────
// In your Claude Code tool handler:
async function callToolWithReceipt(toolName, toolArgs, caller) {
const before = await beforeSkillCall(toolName, toolArgs, caller);
const rawResult = await yourMCPClient.callTool(toolName, toolArgs);
const receipt = await afterSkillCall(before, toolName, rawResult, caller);
return { result: rawResult, receipt };
}
In the Claude Code config (claude_desktop_config.json)
{
"mcpServers": {
"axiom": {
"command": "node",
"args": ["/path/to/mcp-servers/axiom-test-server.js"]
}
}
}
Axiom's MCP server returns receipt metadata alongside tool results. Configure your harness to extract and store receipts on every tool call.
Scenario C — Custom Agent Framework
Integrate as middleware in your existing agent execution loop. Two functions to add to your agent's skill invocation path:
/**
* Called before every skill invocation.
* @param {string} skill - skill name
* @param {object} inputs - skill inputs
* @returns {{timestamp, inputsHash, skill, inputs}}
*/
function beforeSkillCall(skill, inputs) {
const timestamp = new Date().toISOString();
const inputsHash = hashInputs({ skill, args: inputs });
return { timestamp, inputsHash, skill, inputs };
}
/**
* Called after every skill invocation.
* @param {object} beforeState - result of beforeSkillCall
* @param {object} result - skill execution result
* @param {string} [caller] - optional caller ID
* @returns {Promise<object>} - stored receipt
*/
async function afterSkillCall(beforeState, result, caller) {
const outputHash = hashOutput(result);
const signature = signReceipt(
beforeState.skill,
beforeState.timestamp,
beforeState.inputsHash,
outputHash
);
return createReceipt({
skill_name: beforeState.skill,
timestamp: beforeState.timestamp,
inputs_hash: beforeState.inputsHash,
output_hash: outputHash,
signature,
verification_status: 'verified',
source: 'live',
caller: caller || null,
});
}
Wiring into your agent loop
// Inside your agent's skill execution loop:
async function executeSkill(skill, inputs, caller) {
const before = beforeSkillCall(skill, inputs);
const result = await callMCP(skill, inputs); // your MCP call
const receipt = await afterSkillCall(before, result, caller);
return { result, receipt };
}
Authorization: Bearer YOUR_KEY to tag traffic as source: 'live'. Without a key, traffic is tagged as source: 'demo' and excluded from compliance exports.
API Reference
Call a skill and receive a signed execution receipt. Every invocation is recorded — including failures.
Request body
{
"skill_name": "echo", // required — skill/tool identifier
"inputs": { "message": "hello" }, // required — skill inputs as JSON object
"caller": "my-agent-v1" // optional — caller identifier for audit filtering
}
Response
{
"receipt": {
"id": 142,
"skill_name": "echo",
"timestamp": "2026-05-27T08:00:00.000Z",
"inputs_hash": "a1b2c3d4e5f6…",
"output_hash": "f6e5d4c3b2a1…",
"signature": "xK8mP2nLqRvT4wYzAb3c…",
"verification_status": "verified",
"source": "live",
"caller": "my-agent-v1"
},
"result": { "content": [{ "type": "text", "text": "..." }] }
}
Error responses
// 400 Bad Request — missing required fields
{ "error": "skill_name and inputs are required" }
// 502 Bad Gateway — MCP server unreachable
{ "error": "MCP server unavailable", "detail": "...", "hint": "..." }
// Error receipts are still stored (every attempt is auditable)
{ "receipt": { "id": 143, "skill_name": "...", "verification_status": "error" }, "result": { "status": "error", "message": "..." } }
List receipts with optional filters. Results sorted by created_at DESC (newest first).
Query parameters
| Param | Type | Description |
|---|---|---|
skill_name | string | Filter by skill name (exact match) |
source | string | live or demo |
caller | string | Filter by caller identifier |
date_from | YYYY-MM-DD | Receipts created on or after this date |
date_to | YYYY-MM-DD | Receipts created on or before this date |
limit | integer | Max results, 1–200 (default: 50) |
offset | integer | Pagination offset (default: 0) |
# Live receipts for a specific skill, paginated curl "https://axiom-ai-19.polsia.app/api/receipts?skill_name=echo&source=live&limit=20&offset=0" # All receipts from a specific caller in May 2026 curl "https://axiom-ai-19.polsia.app/api/receipts?caller=my-agent-v1&date_from=2026-05-01&date_to=2026-05-31"
Verify a receipt's integrity. Timing-safe HMAC check. No authentication required — anyone can verify any receipt by ID.
curl https://axiom-ai-19.polsia.app/api/receipts/142/verify | jq .
Verification response
{
"receipt_id": 142,
"skill_name": "echo",
"timestamp": "2026-05-27T08:00:00.000Z",
"source": "live",
"caller": "my-agent-v1",
"inputs_hash": "a1b2c3d4e5f6…",
"output_hash": "f6e5d4c3b2a1…",
"signature_valid": true, // HMAC-SHA256 intact
"verification_status": "verified",
"verified": true // signature valid AND not an error receipt
}
verified: false with signature_valid: true indicates a successful call that returned an error result — the signature is valid but the skill execution failed. Check verification_status for the full picture.
List available skills on the configured MCP server. Useful for dynamic agent tool discovery.
curl https://axiom-ai-19.polsia.app/api/receipts/invocations | jq .tools[].name
Receipt Schema
Every receipt is a tamper-evident cryptographic record. The fields below are the complete schema — compliance auditors will read this directly.
| Field | Type | Description | Example |
|---|---|---|---|
id |
integer | Auto-incrementing primary key. Use this for verification lookups. | 142 |
skill_name |
string | Tool/skill identifier as declared in the MCP manifest. | "echo" |
timestamp |
ISO 8601 | UTC time of invocation initiation. Precision to milliseconds. Used as part of the signed payload — tampering changes the signature. | "2026-05-27T08:00:00.000Z" |
inputs_hash |
SHA-256 hex | SHA-256 of canonical JSON of skill inputs. Keys sorted for determinism. Identical inputs always produce identical hash — verifiable offline. | "a1b2c3d4e5f6..." |
output_hash |
SHA-256 hex | SHA-256 of canonical JSON of skill output. If output is a raw scalar, it's stringified first. | "f6e5d4c3b2a1..." |
signature |
base64 | HMAC-SHA256 over skill_name|timestamp|inputs_hash|output_hash. Verifiable independently using the signing secret. |
"xK8mP2nLqRvT4wYzAb3c..." |
verification_status |
enum | "verified" — successful execution. "error" — execution failed (receipt still valid, but skill returned an error). "pending" — not yet verified. |
"verified" |
source |
enum | "live" — authenticated production traffic. "demo" — from the /receipts/demo UI, excluded from compliance exports. |
"live" |
caller |
string | null | Arbitrary caller identifier set at invocation time. Use for filtering by agent ID, user ID, or session. | "my-agent-v1" |
created_at |
ISO 8601 | DB insert timestamp. For log correlation and pagination. | "2026-05-27T08:00:00.123Z" |
Security Model
HMAC Signing
Receipts are signed with HMAC-SHA256 using a secret key. The signing payload is:
signing_payload = skill_name + "|" + timestamp + "|" + inputs_hash + "|" + output_hash signature = base64(HMAC-SHA256(signing_payload, signing_secret))
The signing secret is derived from RECEIPT_SECRET environment variable. In production, set this to a cryptographically random 256-bit value. Rotation: generate a new secret, re-sign active receipts, deploy, then revoke the old secret.
Input/Output Hashing
Hashes use SHA-256 over canonical JSON. Keys are sorted alphabetically before serialization to guarantee determinism across runs and languages:
// Inputs are serialized with keys sorted — identical inputs always produce identical hash
const canonical = JSON.stringify(inputs, Object.keys(inputs).sort());
const inputsHash = crypto.createHash('sha256').update(canonical).digest('hex');
Timing-Safe Verification
Verification uses crypto.timingSafeEqual to prevent timing attacks. String comparison in constant time ensures attackers cannot use response latency to infer the correct signature byte-by-byte.
What Axiom Can't See
Axiom receives the receipt payload (skill name, timestamp, hashes) but not the signing secret. You compute the signature client-side using your copy of the secret and send the result to Axiom for storage. For the highest assurance posture, run Axiom in self-hosted mode where the signing secret stays on your own servers.
Axiom cannot forge a valid receipt because it cannot compute the HMAC without your secret. Verification proves the receipt was created with the same secret used to verify — you control both ends of that chain.
Moving from Demo to Production
Before going live, work through this checklist. Each item is a real compliance requirement that enterprise procurement will ask about.
-
Get a production API key. Demo keys tag traffic as
source: 'demo', which is excluded from compliance exports. Retrieve your production key at /account/billing. -
Point at the production endpoint. Base URL:
https://axiom-ai-19.polsia.app. SetAXIOM_BASE_URLin your agent's env so you can swap endpoints for testing. -
Configure receipt retention policy. Define how long receipts must be kept based on your regulatory requirements. By default, Axiom retains receipts indefinitely. Contact joe@getaifo.com for data retention SLA customization.
-
Set up monitoring on receipt volume. Track
GET /api/receipts?source=livevolume. A spike in error receipts or a drop in total receipts is a sign something is wrong in the invocation path. Set alerts at 2× your baseline. -
Enable audit log export. Export receipts on a schedule using
GET /api/receipts?source=live&date_from=...&date_to=.... Format: JSON Lines (\n-delimited) is recommended for bulk ingestion into your SIEM. -
Rotate the signing secret. Generate a production-strength secret (
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"). Store it in your secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.), not in source code. -
Test the full verify loop. Invoke a skill, fetch the receipt ID, call
GET /api/receipts/:id/verify, confirmverified: true. Do this with your production key and in your deployment environment before the first compliance audit.