The MCP Transparency Problem: When Your Agent Can't Show Its Work
The MCP Transparency Problem: When Your Agent Can't Show Its Work
You ask your AI agent to cancel a subscription, send an email to a client, or update a database record. The agent says "Done." You move on.
But what actually happened? Which API endpoint was called? What payload was sent? What did the service respond? You don't know -- and neither does anyone else. The agent acted on your behalf, and the only record of that action is the agent's own word.
This is the transparency problem in MCP. Every tool call is a black box: an input goes in, a result comes out, and the specifics of what happened between the two are discarded the moment the call completes.
That might be acceptable for a search query. It is not acceptable when the agent is sending emails, processing payments, modifying records, or making API calls that have real-world consequences.
What "transparency" actually means here
Transparency in the context of MCP tool calls is not about seeing source code or inspecting model weights. It is about a concrete, answerable question:
Can anyone -- the user, the operator, a regulator, the other party -- independently verify what the agent did?
Today, the answer is no. Here is why.
The self-reporting problem
A standard MCP server handles a tool call like this:
@server.call_tool()
async def handle_tool(name: str, arguments: dict):
if name == "cancel_subscription":
resp = await httpx.post(
"https://api.stripe.com/v1/subscriptions/sub_1234",
data={"cancel_at_period_end": "true"},
headers={"Authorization": f"Bearer {STRIPE_KEY}"},
)
return {"status": "cancelled", "effective": "end_of_period"}
The user sees {"status": "cancelled"}. That is the tool's self-report. The HTTP response from Stripe -- the actual evidence -- was consumed and discarded inside the server process.
Three problems with this:
- The claim is unverifiable. The user cannot confirm the request was actually sent to Stripe, or what Stripe actually responded.
- The record is mutable. If the server logs the action, those logs are written by the same process that executed it. They can be edited, truncated, or were never written if the process crashed.
- The timestamp is self-reported. The server says the call happened at 14:03. Nobody independent certifies that.
Every downstream consumer of this tool call's result -- the user, the orchestrator, the compliance system -- is operating on trust. Not verified trust. Assumed trust.
Why logging doesn't solve this
The immediate instinct is to add logging:
import logging
logger = logging.getLogger("mcp-tools")
@server.call_tool()
async def handle_tool(name: str, arguments: dict):
if name == "cancel_subscription":
resp = await httpx.post(stripe_url, data=payload, headers=headers)
logger.info(f"cancel_subscription called at {datetime.utcnow()}, "
f"stripe responded {resp.status_code}")
return {"status": "cancelled"}
This is better than nothing. But the log has a fundamental problem: it was written by the same entity that performed the action. This is the equivalent of a company auditing itself.
In any system where accountability matters -- finance, healthcare, legal, multi-party operations -- self-reported records are not evidence. They are claims. The distinction is not academic. It is the difference between "we say we did it" and "here is proof we did it, verifiable by anyone."
The three-party transparency pattern
To make a tool call transparent, you need a witness that is independent of both the agent and the upstream service. The pattern looks like this:
Agent → Verification Proxy → Upstream API
↓
Cryptographic Receipt
(signed, timestamped, logged)
The proxy forwards the request to the upstream API unchanged. But it captures the exact request and response bytes, then produces a receipt with three independent attestations:
- A digital signature (Ed25519) -- proving the proxy witnessed this exact exchange
- A third-party timestamp (RFC 3161) -- proving when the exchange happened, certified by an independent Time Stamping Authority
- A transparency log entry (Sigstore Rekor) -- proving the receipt existed at a specific point in time, in a public, append-only log maintained by the Linux Foundation
No single party -- not the agent, not the proxy, not the upstream API -- can forge this combination.
Adding transparency to an MCP server
Here is the same subscription cancellation, routed through a certifying proxy:
TRUST_PROXY = "https://trust.arkforge.tech/v1/proxy"
ARKFORGE_KEY = "your_api_key"
@server.call_tool()
async def handle_tool(name: str, arguments: dict):
if name == "cancel_subscription":
resp = await httpx.post(
TRUST_PROXY,
headers={"X-Api-Key": ARKFORGE_KEY},
json={
"target": "https://api.stripe.com/v1/subscriptions/sub_1234",
"method": "POST",
"payload": {"cancel_at_period_end": "true"},
"extra_headers": {"Authorization": f"Bearer {STRIPE_KEY}"},
},
)
data = resp.json()
return {
"status": "cancelled",
"effective": "end_of_period",
"_proof_id": data["proof"]["proof_id"],
}
The upstream API still receives the identical request. Stripe still processes the cancellation exactly the same way. The only difference: a neutral third party now holds a signed, timestamped, publicly logged record of exactly what was sent and what came back.
The _proof_id returned to the user is a handle they can use to verify the action independently -- without trusting the agent, the server, or the proxy.
Anatomy of a receipt
The proxy returns a proof object alongside the original API response:
{
"proof_id": "prf_20260406_140312_b7d2e4",
"spec_version": "1.2",
"timestamp": "2026-04-06T14:03:12Z",
"hashes": {
"request": "sha256:a4f1...3c8b",
"response": "sha256:d920...7e1a",
"chain": "sha256:6b3e...91f0"
},
"parties": {
"buyer_fingerprint": "sha256:your_api_key_hash",
"seller": "api.stripe.com"
},
"arkforge_signature": "ed25519:KjG8...rQ==",
"arkforge_pubkey": "ed25519:ZLlG...fEY",
"timestamp_authority": {
"status": "verified",
"provider": "freetsa.org"
},
"transparency_log": {
"provider": "sigstore-rekor",
"status": "success",
"entry_uuid": "24296fb5...",
"verify_url": "https://search.sigstore.dev/?logIndex=1217489868"
},
"verification_url": "https://trust.arkforge.tech/v1/proof/prf_20260406_140312_b7d2e4"
}
The chain hash binds the request hash, response hash, timestamp, and party identifiers into a single value using canonical JSON serialization. Changing any field invalidates the chain. The chain hash is what gets signed, timestamped, and logged.
Verifying without trusting anyone
Verification requires math, not trust. Here is how any party -- the user, an auditor, the other side of the transaction -- can verify a receipt independently:
import hashlib, json, httpx
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from base64 import urlsafe_b64decode
# 1. Fetch the proof by ID
proof = httpx.get(
"https://trust.arkforge.tech/v1/proof/prf_20260406_140312_b7d2e4"
).json()
# 2. Recompute the chain hash
chain_input = {
"request_hash": proof["hashes"]["request"],
"response_hash": proof["hashes"]["response"],
"transaction_id": proof["proof_id"],
"timestamp": proof["timestamp"],
"buyer_fingerprint": proof["parties"]["buyer_fingerprint"],
"seller": proof["parties"]["seller"],
}
canonical = json.dumps(chain_input, sort_keys=True, separators=(",", ":"))
expected = "sha256:" + hashlib.sha256(canonical.encode()).hexdigest()
assert expected == proof["hashes"]["chain"], "Chain hash mismatch"
# 3. Verify the Ed25519 signature
pubkey_bytes = urlsafe_b64decode(proof["arkforge_pubkey"].split(":")[1] + "=")
pubkey = Ed25519PublicKey.from_public_bytes(pubkey_bytes)
sig_bytes = urlsafe_b64decode(proof["arkforge_signature"].split(":")[1] + "=")
pubkey.verify(sig_bytes, proof["hashes"]["chain"].split(":")[1].encode())
# 4. Confirm the Rekor entry exists (public transparency log)
rekor_uuid = proof["transparency_log"]["entry_uuid"]
rekor_resp = httpx.get(
f"https://rekor.sigstore.dev/api/v1/log/entries/{rekor_uuid}"
).json()
log_index = list(rekor_resp.values())[0]["logIndex"]
print(f"Verified. Rekor log index: {log_index}")
If step 2 passes, the chain hash matches its declared inputs -- nothing was tampered with. If step 3 passes, the proxy signed that exact chain hash with a key the agent never held. If step 4 passes, the hash was committed to a public log before anyone knew it would be checked.
This is what transparency means in practice: not a promise, but a proof that any party can verify without asking permission.
Three scenarios where this matters
1. Customer disputes
An agent sends an invoice reminder email via SendGrid. The customer claims they never received it. Without a receipt, you have the agent's self-report against the customer's claim. With a receipt, you have cryptographic proof of the exact payload sent to SendGrid and SendGrid's exact response -- timestamped and signed by an independent authority.
2. Multi-agent handoffs
Agent A fetches pricing data from an API. Agent B uses that data to generate a quote. The quote is wrong. Was the pricing data stale? Did Agent A fetch the wrong endpoint? Did Agent B misinterpret the response? Without receipts at each handoff, debugging is guesswork. With receipts, each agent's inputs and outputs are independently verifiable -- the chain of evidence is complete.
3. Regulatory audits
An auditor asks: "Prove that your AI agent's actions on March 15th complied with your stated policy." Without receipts, you hand over server logs that you wrote and control. With receipts, you hand over a set of proof IDs that the auditor can verify against a public transparency log -- without needing access to your systems.
What it costs
The free tier covers 500 receipts per month. No credit card required. Each receipt adds roughly 200ms of latency (proxy round-trip plus timestamp authority verification). For most MCP tool calls -- API integrations, emails, webhooks, database operations -- that overhead is negligible compared to the upstream call itself.
For production workloads: plans start at EUR 29/month for 5,000 receipts.
When to add receipts
Not every tool call needs a receipt. A search_web call probably doesn't. But any tool call where the result could be disputed, audited, or questioned by another party is a candidate.
The decision heuristic: if the answer to "prove it" matters, add a receipt.
Payments. Emails. Data mutations. Cross-organization API calls. Regulatory submissions. Anything where "the agent said it did it" is not sufficient evidence.
The transparency gap is structural
MCP gives agents a clean, standardized way to invoke tools. That is a significant step forward. But the protocol says nothing about proving what happened during a tool call. It captures inputs and outputs at the protocol level but discards the evidence of what occurred between the tool server and the upstream API.
This is not a bug in MCP. It is a gap that the protocol was not designed to fill. Transparency is infrastructure -- it needs to be added deliberately, the same way TLS was added to HTTP or signatures were added to package managers.
Cryptographic receipts are the mechanism. A certifying proxy is the deployment pattern. And the cost of adding them -- three lines of code, sub-second latency -- is negligible compared to the cost of operating agents that cannot prove what they did.
The ArkForge Trust Layer is an open-architecture certifying proxy for MCP and API calls. The proof specification is public. The verification algorithm requires no proprietary software. Start free -- 500 proofs/month, no card required.
Prove it happened. Cryptographically.
ArkForge generates independent, verifiable proofs for every API call your agents make. Free tier included.
Get my free API key → See pricing