audit event mcp
@kajaril/audit-event-mcp
Agent steward infrastructure — hash-chained audit log with Merkle notarisation and a GDPR/AI-Act compliance skill.
What it does
Records agent events (tool calls, decisions, human turns) to a per-client SQLite log with a SHA-256 hash chain — every record is cryptographically linked to the previous one
Optionally notarises batches with a Merkle root + Ed25519 signature (paid tier) — external auditors can verify without contacting kajaril
Exports all events for a data subject as NDJSON on demand (GDPR Art. 20 portability)
Ships a Claude Code skill that runs 8 GDPR/AI-Act axes against any event store
Related MCP server: Agent Receipts
MCP endpoint
https://audit-event.kajaril.com/mcpJSON-RPC 2.0 over HTTPS, authenticated via Cloudflare Access (service token) or an OAuth client-credentials access token — see Authentication. Contact studio@kajaril.com for onboarding.
Tools
Tool | Description |
| Write one audit event. Returns |
| Recompute |
| Filter by session, agent, event type, or date range. |
| Export all events for a |
| Ask a human to approve an action. Returns |
| Poll an approval: |
Quick start
Record an event:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "record_event",
"arguments": {
"agentId": "travel-assistant",
"eventType": "tool.call",
"purpose": "User requested flight search via travel assistant",
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"input": { "tool": "search_flights", "origin": "LHR", "dest": "JFK" },
"lawfulBasis": "contract",
"subjectId": "user-8821"
}
}
}Verify the chain:
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "verify_chain", "arguments": { "limit": 100 } } }Export a data subject's records:
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": { "name": "export_dossier", "arguments": { "subjectId": "user-8821" } } }Authentication
Three ways in; all end at a signature-verified client_id that selects your tenant —
nothing user-supplied ever does.
Cloudflare Access service token (manual onboarding): send the token headers, CF Access injects a verified JWT. Full surface.
OAuth browser flow (MCP clients, zero copy-paste): point your client at the server and let it discover the rest —
claude mcp add --transport http audit-event https://audit-event.kajaril.com/mcpRFC 8414 metadata advertises dynamic client registration (/oauth/register),
authorization with PKCE S256 (/oauth/authorize), and the shared token endpoint. Consent
is approved in the browser by a signed-in tenant operator; the granted scope (agent or
admin) is bound into the issued token.
OAuth client-credentials (M2M): exchange a client secret for a 1-hour Bearer token at
POST /oauth/token (application/x-www-form-urlencoded, HTTP Basic or client_id /
client_secret body params):
curl -s https://audit-event.kajaril.com/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials -d scope=agentTwo scopes, issued and rotated independently:
Scope | May call |
|
|
| everything, plus |
Secrets are shown once at issue/rotation (POST /credentials/rotate, body {"scope":"agent"})
and stored only as hashes in your tenant's Durable Object. Rotating a scope invalidates its
old secret immediately; outstanding access tokens expire within the hour.
The approval tools are also plain REST for production backends: POST /approvals (same body
as request_approval) and GET /approvals/:id, with Authorization: Bearer <access token>.
How it works
Hash chain
Every event commits to all events before it. The chain is recomputable from first principles:
input_hash_slot = input_hash ?? input_hash_omitted_reason
chain_hash = SHA-256(id + "|" + event_type + "|" + input_hash_slot + "|" + prev_hash)When a caller supplies inputHashOmittedReason instead of input, the reason string enters the chain — the omission itself is tamper-evident.
Merkle notary (paid tier)
Every 15 minutes (or at 1,000 pending events), the notary Worker:
Collects
{ id, chain_hash }pairs from pending recordsSorts them by id and builds a SHA-256 binary Merkle tree
Signs the root with Ed25519
Writes
merkle_root+notary_sigback to each record
The notary public key is published at /.well-known/notary-pubkey (also proxied
same-origin on go.kajaril.com for the verify page). Any auditor can verify signatures
offline without contacting kajaril. The notary never receives input_hash, payload_ref,
or any payload content.
Verifying a dossier
export_dossier returns a link to a human-readable dossier on go.kajaril.com with a
raw JSONL evidence file attached. Anyone can drop that file onto
https://go.kajaril.com/verify — the browser recomputes every record's chain
fingerprint from its exported preimage (id | event_type | input_hash ?? omitted_reason | prev_hash) and checks each notary Ed25519 signature against the published key. Nothing is
uploaded; the verdict is computed client-side.
Privacy design
inputis hashed locally; the raw value is not stored unless an R2 bucket is explicitly boundinput_hashis excluded fromquery_eventsresponses; dossiers export it (withprev_hash) as the chain preimage that makes independent verification possible — they are digests, never contentpayload_refis never returned to callers — enforced at every handler boundaryAn empty
export_dossierresult (eventCount: 0) is valid GDPR evidence that no data exists for a subject
Event types
tool.call | tool.result | decision.made |
human.turn | memory.read | memory.write | error.raised |
approval.requested | approval.decidedapproval.* types are reserved: the witness records them itself when a human approval is
requested and decided. record_event refuses them — an agent must never be able to fabricate
human-decision evidence. (approval.deferred and approval.escalated are reserved for later.)
Decision webhooks
When an approval carries a callback_url, the decision (approve/deny) is POSTed to it the
moment a human decides — this is what lets an interrupted agent resume instead of poll.
Timeouts never fire a webhook; polling resolves those.
Every delivery is signed. The body is JSON:
{
"type": "approval.decided",
"approval": { "id": "…", "status": "approved", "reason": null, "responderId": "…",
"agentId": "…", "sessionId": "…", "actionSummary": "…",
"actionPayloadHash": "…", "createdAt": "…", "decidedAt": "…", "expiresAt": "…" },
"chainEvent": { "id": "…", "chainHash": "…" }
}chainEvent points at the approval.decided entry in the hash chain — cite it as evidence.
Verifying the signature. Your webhook secret (whsec_…) is returned in every
request_approval response as webhookSecret — no dashboard needed. Each delivery carries:
X-Kajaril-Signature: t=<unix seconds>,v1=<hex>To verify:
Read
tandv1from the header. Reject if|now − t|exceeds 300 seconds.Compute
HMAC-SHA256(key = the literal secret string (UTF-8, whsec_ prefix included), message = "{t}." + raw request body).Hex-encode and compare to
v1with a constant-time comparison.
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));The reference implementation is verifyWebhookSignature in src/lib/webhook.ts — the same
code path our tests run.
Self-hosting note: webhook signing derives per-tenant secrets from the WEBHOOK_SIGNING_SECRET
Workers Secret. If it is unset, webhooks are not sent at all (never unsigned) and
request_approval returns webhookSecret: null; polling still works.
Notification channels
request_approval accepts channels: ["telegram", "email"] and notifies the approver over a
ladder: instant first, email as the fallback.
Telegram (instant). Connect once — POST /channels/telegram/connect (admin scope) returns a
one-time t.me deep link (15-minute expiry). Open it, press Start, and approval cards arrive in
that chat with inline Approve / Deny / Deny with reason buttons. Deny-with-reason
prompts for a short reply that is recorded with the decision — stronger Art. 14 evidence than a
binary stamp. Connecting again replaces the bound chat.
Email (fallback). Set the approver address once — POST /channels/email (admin scope) with
{"address": "…"}. The email fires only when it is needed:
immediately, if no instant channel delivered (not connected, send failed, or not requested);
after 10 minutes, if an instant card was delivered but the approval is still undecided;
never, if the approval would expire before the email could matter.
Mail is sent from approvals@send.kajaril.com with the decide link; replies reach a human via
studio@kajaril.com.
Dispatch report. Every request_approval response includes what actually happened:
"notifications": { "telegram": "sent", "email": "scheduled" }(telegram: sent | failed | not_connected | unconfigured | not_requested; email:
immediate | scheduled | expires_first | no_address | unconfigured | arm_failed | not_requested.)
TTL defaults follow the channels: 30 minutes when an instant channel is in play, 4 hours when
the request is email-only, explicit ttlSeconds always wins.
Self-hosting note: channels are optional. Without TELEGRAM_BOT_TOKEN / RESEND_API_KEY /
CHANNELS_KV, dispatch reports unconfigured and the approval still works through
approvalUrl + polling. The bindings are documented in wrangler.jsonc and wrangler.go.jsonc.
Lawful basis (GDPR Art. 6)
legitimate_interest | contract | legal_obligation |
vital_interest | public_task | consentProvide lawfulBasis for any event that processes personal data. Omit it for events that do not.
Tiers
Capability | Free | Paid |
Hash-chained event writes | yes | yes |
| yes | yes |
| yes | yes |
R2 payload storage | optional | optional |
Merkle root + Ed25519 notarisation | — | yes |
compliance-audit axis 6 (notary coverage) | skipped | evaluated |
compliance-audit skill
skills/compliance-audit/SKILL.md is a Claude Code skill that runs 8 GDPR/AI-Act compliance axes against any event store. Copy it into your workspace:
cp -r node_modules/@kajaril/audit-event-mcp/skills/compliance-audit .claude/skills/Axes:
Lawful basis present — GDPR Art. 5–6
Purpose specificity — GDPR Art. 5(1)(b)
Subject linkage on
human.turneventsRetention bounded (≤ 730 days)
Chain integrity — 10% sample recompute
Notary coverage ≥ 95% (paid tier only)
High-risk event completeness — AI Act Art. 12–13
Data minimisation signal —
payload_reffraction
Each axis produces pass | fail | warn with an evidence snippet. Output is JSON + a markdown summary block.
Self-hosted deployment
Prerequisites
Cloudflare account with Workers and Durable Objects enabled
wranglerCLI authenticated
Steps
# 1. Create R2 bucket
wrangler r2 bucket create audit-payloads
# 2. Set notary signing key (paid tier)
wrangler secret put NOTARY_PRIVATE_KEY --config wrangler.notary.jsonc
# Value: hex-encoded Ed25519 private key — never committed to source
# 3. Deploy Workers
wrangler deploy
wrangler deploy --config wrangler.notary.jsonc
# 4. Add audit-event.kajaril.com as a CF Access Self-hosted Application
# Issue one service token per client with custom.client_id claim
# 5. Onboard a client
npx tsx src/scripts/onboard-client.ts --client-id acme-crm --tier free --region eu
# 6. Verify
curl https://audit-event.kajaril.com/healthNever run wrangler deploy from a dirty working tree. The deployed code must match git HEAD.
Development
npm install
npm test # 81 tests (node:sqlite adapter, no cloudflare/vitest-pool-workers)
npm run typecheck
npm run lintLicense
MIT. Copyright © 2026 Kajaril Ltd.
This server cannot be installed
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Flicense-qualityCmaintenanceTamper-evident cryptographic audit trail for LLM outputs. Compliance logging for AI agent decisions.
- AlicenseAqualityCmaintenanceCryptographic accountability for AI agents. Ed25519-signed receipts for every MCP tool call. Constraints, chains, AI judgment, invoicing, and local dashboard included.24111MIT
- Alicense-qualityAmaintenanceVendor sovereignty scanner for EU AI Act compliance. Scans provider stacks for CLOUD Act exposure, data residency, and GDPR DPA status across 55 providers, then suggests EU-native alternatives. Free, no account required.121MIT
- AlicenseBqualityBmaintenanceHash-chained HMAC-signed audit log MCP for A2A (agent-to-agent) calls. Every tool-call, agent-handoff, decision gets a tamper-evident signed record.5MIT
Related MCP Connectors
Bitcoin-anchored, tamper-evident audit log for AI agents — record, disclose and verify actions.
Hash-chained HMAC-signed audit log MCP for A2A (agent-to-agent) calls. Every tool-call, agent-ha...
Tamper-evident audit log service for agent-to-agent transactions
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/mightbesaad/audit-event-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server