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:

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.

python
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:

python
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

bash
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

References