MCP Security
Model Context Protocol turns any LLM into an agent by exposing tools and resources over a uniform interface. That uniformity is also a governance surface: one policy layer can sit in front of every MCP call, sign the invocation, gate it on approval, and record it as tamper-evident evidence.
This page maps the MCP threat surface to real Asqav features. Each claim below points at a concrete endpoint, module, or config value you can inspect.
Threat surface
There is no formal "OWASP MCP Top 10" spec. The authoritative references are the OWASP Top 10 for LLM Applications 2025 and the OWASP Agentic AI Threats & Mitigations v1.0. Both apply directly to MCP deployments. The items that most often go unaddressed in an MCP server are:
- LLM06 Excessive Agency. The agent calls a tool that writes to prod, and there is no secondary check. One prompt-injected instruction is enough to do real damage.
- LLM05 Improper Output Handling. A tool result is passed back to the model without validation or scanning, so an attacker-controlled response becomes the next prompt.
- LLM01 Prompt Injection. Instructions embedded in tool responses (SQL rows, documents, web pages) override the system prompt.
- LLM02 Sensitive Information Disclosure. A tool returns PII or credentials that flow back into model context and into logs.
- Tool definition integrity. The
tools/listresponse is mutable. Nothing prevents a malicious or compromised MCP server from redefining what a tool does after the agent trusted it.
What Asqav governs
1. Signed tool invocations
Every tool call becomes an Asqav action. The action payload (tool name, arguments hash, timestamp) is signed with ML-DSA-65 (NIST FIPS 204) before the tool runs. The signature plus action id is stored as a signature_record that is publicly verifiable.
from asqav import Agent
agent = Agent.create("mcp-router", algorithm="ML-DSA-65")
# In your MCP server, before calling the underlying tool
sig = agent.sign(action={
"tool": "database.query",
"args": {"sql": "SELECT * FROM users WHERE id = $1", "params": [user_id]},
})
result = await run_tool(sig.action)
# result is now tied to a verifiable signature record:
# https://api.asqav.com/api/v1/verify/<signature_id>
2. Approval-gated invocations
High-impact tool calls do not run until a human approves. Asqav Approvals supports policy-driven queues where invocations matching a pattern (tool name, argument regex, resource scope) pause at sign() until an authorized reviewer approves. This is the direct control for LLM06 Excessive Agency: the agent literally cannot execute until the gate lifts.
3. Quarantine mode instead of silent fix
When a guardrail catches a policy violation, Asqav offers three outcomes: log, quarantine, block. Quarantine suspends the specific agent session, preserves the evidence, and notifies reviewers. No silent output mutation.
4. Scanning on both sides of the call
Asqav Guardrails scans both prompts and tool results, covering LLM01 prompt injection and LLM02 sensitive-information disclosure, so a bad tool response can be caught before it reaches the model.
5. Audit bundles
The Compliance Reports endpoint assembles signed bundles: every tool invocation in a session, each with its ML-DSA signature, an OpenTimestamps Bitcoin anchor on all plans and, on Enterprise, an RFC 3161 qualified timestamp when the external witness is reachable, plus policy decisions and approval records. Auditors can verify these offline with the Asqav public key.
6. Revocation without key rotation
If an MCP client is compromised, revoke the agent with DELETE /api/v1/agents/<agent_id>. Any signature produced after revocation is rejected by the public verify endpoint, even if the attacker still holds the signing key. No credential rotation needed.
MCP middleware pattern
A minimal FastMCP server with Asqav in front of every tool call:
from mcp.server.fastmcp import FastMCP
from asqav import Agent
mcp = FastMCP("database-agent")
agent = Agent.create("database-agent", algorithm="ML-DSA-65")
@mcp.tool()
async def query_users(user_id: str) -> dict:
# Sign first, run second. If policy requires approval, sign() blocks.
sig = agent.sign(action={"tool": "query_users", "args": {"user_id": user_id}})
# At this point the action is durable: signed, timestamped, audit-ready.
# Asqav has already enforced scanning + quarantine rules.
rows = await db.fetch("SELECT id, email FROM users WHERE id = $1", user_id)
# Bind the result to the signature so downstream can trace provenance
return {"signature_id": sig.id, "rows": rows}
Verify an MCP signature in 30 seconds
curl https://api.asqav.com/api/v1/verify/<signature_id>
Returns the full signed payload plus anchor status (both plans anchor to Bitcoin via OpenTimestamps. Enterprise adds RFC 3161 timestamps per RFC 9882). Third-party auditors need no Asqav account to verify, which makes the evidence portable across compliance frameworks.
Further reading
- Agents - lifecycle, keys, revocation
- Policies - writing gate rules for tool patterns
- Compliance Reports - export audit bundles for EU AI Act Article 12, DORA, SOC 2
- Shadow AI capture - sign outbound LLM calls at the corporate egress proxy, no SDK change required
- Blog: AI agent security in the MCP era