MCP Security
Updated
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: you can sign each invocation 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. It describes asqav 0.10.10, the release on PyPI.
Where the check runs
Asqav does not sit in front of an MCP server by itself. Two paths put it on the call path, and they cover different calls.
The first is your own server code. The sign() calls on this page run inside your MCP server, so they cover only the tools whose code makes the call. A tool that does not call sign() is not signed, and neither is a call that goes around the tools you wrapped.
The second is the Claude Code hook. If you run the agent in Claude Code, asqav hook pretool runs from the harness before each tool call. With the matcher * it fires for every tool call that goes through Claude Code, including MCP tools, which Claude Code names mcp__<server>__<tool>. It needs no change to the MCP server. The Enforcement page says what the hook can and cannot stop in 0.10.10.
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
Each tool whose code calls sign() becomes an Asqav action. The action payload (tool name, arguments hash, timestamp) is signed with ML-DSA-65 (NIST FIPS 204). Put the call before the tool body: if signing is refused, sign() raises and the tool does not run. The signature plus action id is stored as a signature_record that is publicly verifiable.
import asqav
asqav.init() # reads ASQAV_API_KEY
agent = asqav.Agent.create("mcp-router")
# In your MCP server, before calling the underlying tool
sig = agent.sign("tool:database_query", {
"tool": "database_query",
"sql": "SELECT * FROM users WHERE id = $1",
"params": [user_id],
})
result = await run_tool("database_query", user_id)
# result is now tied to a verifiable signature record:
# https://api.asqav.com/api/v1/verify/<signature_id>
2. Approvals for high-impact calls
Asqav Approvals records a person's decision about a signed action. It does not pause the agent: creating or deciding an approval does not pause or resume execution, and sign() does not wait for a reviewer. For a high-impact tool, your server creates the approval request and checks the decision before it runs the tool. That is the control for LLM06 Excessive Agency, and it holds only for the tools whose code does this.
Asqav signs a decision with your organization's key and checks it against the key the organization publishes at /.well-known/jwks.json, never against a key stored with the record. Check verified on the decision before you act on it. How decisions are signed covers the details, including the decisions labelled legacy_unverifiable.
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 signed tool call in the session, each with its ML-DSA signature, an OpenTimestamps Bitcoin anchor on all plans and, on Pro, Business and Enterprise, an RFC 3161 token from an EU QTSP endpoint when the external witness is reachable, verified by eKU, pinned-root chain and validity window, plus policy decisions. Auditors can verify each receipt offline against the signing key published at /.well-known/jwks.json. Approval decisions are not part of these bundles; check one at /api/v1/verify/{approval_id}, which verifies it against the organization's published key. Asqav does not check trusted-list membership or certificate policy at the relevant time, so each receipt does not establish qualified status.
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 MCP server (Python SDK mcp 2.x) where one tool signs before it runs:
import asqav
from mcp.server.mcpserver import MCPServer
asqav.init() # reads ASQAV_API_KEY
mcp = MCPServer("database-agent")
agent = asqav.Agent.create("database-agent")
@mcp.tool()
async def query_users(user_id: str) -> dict:
# Sign first, run second. If signing is refused or fails, sign()
# raises and the query below never runs.
sig = agent.sign("tool:query_users", {"user_id": user_id})
rows = await db.fetch("SELECT id, email FROM users WHERE id = $1", user_id)
# Return the signature id so downstream can trace provenance
return {"signature_id": sig.signature_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 (every plan anchors to Bitcoin via OpenTimestamps; Pro, Business and Enterprise add 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
References
- Model Context Protocol specification, Model Context Protocol
- OWASP Top 10 for LLM Applications, OWASP GenAI Security Project
- FIPS 204: Module-Lattice-Based Digital Signature Standard, NIST
- OpenTimestamps, OpenTimestamps
- RFC 3161: Time-Stamp Protocol, IETF
- Regulation (EU) 2024/1689 (EU AI Act), EUR-Lex
- Regulation (EU) 2022/2554 (DORA), EUR-Lex
- SOC 2, AICPA