GUIDE · MCP 10 min ·

What the NSA's MCP security guidance actually requiresThe May 2026 CSI, read as a build spec.

In May 2026 the NSA published security guidance for the Model Context Protocol. It is not abstract policy. It reads like a code review of the server you are about to deploy, and most of its recommendations map to one config change each.

TL;DR· the answer, in twenty seconds

What happened: In May 2026 the NSA's Artificial Intelligence Security Center published security guidance for MCP (CSI U/OO/6030316-26). It arrived after a year of MCP CVEs and reads like a build spec, not a policy memo.

The minimum fix: Log every tool and model invocation with its parameters and the identity behind it, then pipe that to your SIEM. Split MCP servers into a public-data zone and a sensitive-data zone, and keep private data on a local server.

The lesson: The sheet's through-line is least privilege plus an audit trail at the point of action. MCP inverts the client-server pattern, so the controls move to where the server acts, not where the client asks.

In May 2026 the NSA published a security sheet for the Model Context Protocol, and it reads less like policy than like a code review of the server you are about to deploy. The document runs through the protocol's weak spots and attaches a control to each one. Most of those controls cost a config change. This walks the sheet and turns its recommendations into things you can set this week.

What landed, and why the author matters

The full title is "Model Context Protocol (MCP): Security Design Considerations for AI-Driven Automation," published by the NSA's Artificial Intelligence Security Center as CSI U/OO/6030316-26 (PP-26-1834), Version 1.0, dated May 2026. The agency announced it through its press room and its @NSACyber account on May 21, and the press picked it up within the day.

The author is the signal. The NSA does not write hardening guides for hobby protocols. It writes them when a technology crosses into government workflows and critical infrastructure, and MCP got one inside eighteen months of existing. That timing tracks a year of disclosures. OX Security documented a systemic command-execution flaw in the STDIO transport across Anthropic's reference SDK, which The Register reported as putting up to 200,000 server instances at risk. Endor Labs scanned 2,614 implementations and found 82% touching file operations and a large share carrying injection risk. The CSI is the institutional answer to that pile.

It does not patch anything. It tells you what to assume and what to log, and it is candid that the protocol shipped underspecified. The value is that an agency with no product to sell named the controls, so you can argue for them in a design review without sounding paranoid.

The inversion the sheet keeps circling

The CSI returns to one structural fact: MCP reverses the interaction pattern most defenses were built around. In a normal client-server setup, clients request data from servers. MCP often expects servers to query data and execute actions for the connected client. That inversion creates attack paths that traditional cyber defense does not trace, because the server is now the actor and the client is the thing being acted upon.

Two consequences follow. The first is session handling. The sheet flags weaknesses around authentication and session lifecycle that lead to message replay and reuse of valid sessions. Hijack a session and you can impersonate a legitimate client, inject prompts, and talk to the server without tripping anything. Known token passthrough weaknesses live in the specification itself, which is why the CSI treats them as a property of MCP rather than a bug in one server.

Replay is concrete once you picture it. An MCP session is a sequence of JSON-RPC messages over a transport. If the messages carry no expiration and no nonce, a message captured off a compromised channel stays valid, and a server with no replay check will run it again. A tools/call that transferred funds or rotated a key is a message an attacker wants to send twice. The protocol does not stop that on its own, which is why the sheet treats message freshness as something you add rather than something you inherit.

The second consequence is the sheet's bluntest line: conventional controls remain necessary but are not sufficient. Authentication, authorization, and input validation are still required. They do not cover the new risks, because dynamic tool invocation, implicit trust between components, and shared context create exposure that none of the old playbooks anticipated. The OWASP Top 10 for Agentic Applications names the same gap from the other direction. You harden the old surface and the agentic surface is still open.

Log every tool and model invocation

The sheet's heaviest emphasis falls on observability, and this is where most deployments fail. The CSI asks you to log all tool and model invocations: the exact parameters, the identities involved, and where feasible a cryptographic hash of the result or output. Those logs are the backbone of forensic response, and the sheet wants them piped into existing security monitoring such as a SIEM rather than left in a file nobody reads.

Walk into a typical MCP setup and you find none of this. The server prints a startup banner and maybe an error stack, and the tool calls vanish. When something goes wrong you have a model transcript and no record of what the server did with the arguments. The fix is a wrapper around every tool that records the call before it runs and the outcome after:

import hashlib, json, logging, time

audit = logging.getLogger("mcp.audit")  # ship this handler to your SIEM

def logged_tool(identity, name, fn):
    def wrapper(**params):
        start = time.time()
        record = {
            "ts": start,
            "identity": identity,        # who or what made the call
            "tool": name,
            "params": params,            # the exact arguments
        }
        try:
            result = fn(**params)
            record["result_sha256"] = hashlib.sha256(
                json.dumps(result, default=str).encode()
            ).hexdigest()
            record["status"] = "ok"
            return result
        except Exception as exc:
            record["status"] = "error"
            record["error"] = str(exc)
            raise
        finally:
            record["duration_ms"] = int((time.time() - start) * 1000)
            audit.info(json.dumps(record))
    return wrapper

The hash matters more than it looks. The full output does not belong in your logs, because the output is often the sensitive thing. A hash lets you prove later that a given call returned a given result without storing the result, which is the difference between an audit log you can keep and one your own retention policy forbids. The tamper-evident audit log pattern extends this to make the records themselves hard to edit after the fact.

Logs you never query are storage, not security. The point of the SIEM integration the sheet asks for is that someone, or some rule, watches the stream. A tool that has read four files in its lifetime and then reads four hundred is an anomaly your rules can catch only if the four hundred reads are in the log with identities attached. The CSI is explicit that the parameters and the identity go in the record, because a count of calls without who made them tells you a storm happened but not where it started.

A broker that sits between the agent and the credential gets this logging for free, because it sees every request: which process asked, for what, and when. hasp writes that line as a side effect of handing over a scoped reference, so the audit trail the NSA wants exists whether or not the server author remembered to build one.

Zone tools by data classification

The next recommendation is a data-handling rule borrowed from how the government classifies information. Align tools and models with data classification zones. Public tools handle public datasets, like weather or documentation lookups. Tools that touch sensitive or regulated data, meaning national security information, controlled unclassified information, health records, or financial data, get explicit control and segregation.

In practice this means you stop running one agent session that can reach both a public web-fetch tool and your production database tool. The blast radius of a poisoned context is the union of every tool in scope, so a session that holds a public scraper and a customer-data query is a session where a prompt injection in a scraped page can pivot to the customer data. Split the servers and the pivot has nowhere to go:

// public-zone agent profile: read-only, internet-facing tools only
{
  "mcpServers": {
    "docs-search": { "command": "mcp-docs", "env": {} },
    "web-fetch":   { "command": "mcp-fetch", "env": {} }
  }
}

// sensitive-zone agent profile: started separately, never alongside the above
{
  "mcpServers": {
    "customer-db": { "command": "mcp-postgres", "env": {} }
  }
}

The sheet pairs zoning with a sharper rule for private data: prefer a local instance of the MCP server. When the data is private, do not reach a remote server that proxies it across a network you do not control. Run the server on the same host as the agent, keep the data on the loopback, and the network attack surface for that zone drops to zero. Local-first is not a preference here. The NSA lists it as the way to reduce risk when private data is in play, which lines up with the Five Eyes guidance on agentic AI published the same month.

Validate parameters as context, not shape

Input validation gets a redefinition in the CSI. The sheet says parameter validation extends beyond checking input. It means understanding the schema, the expected ranges, and the intended context or configuration where the data will be processed. You check for malformed inputs, missing fields, and excessive sizes, any of which can trigger unstable behavior or feed a prompt injection.

The distinction is between "is this a string" and "is this a string that makes sense for what this tool is about to do." A path parameter that passes a type check can still be ../../etc/passwd. A command argument that is valid UTF-8 can still be a shell metacharacter waiting for a server that builds a command line by concatenation, which is the command injection pattern that accounts for a large slice of MCP CVEs. Validation that understands context rejects the input that is well-formed and wrong:

import re
from pathlib import Path

DOC_ROOT = Path("/srv/docs").resolve()
SLUG = re.compile(r"^[a-z0-9-]{1,64}$")  # expected range, not just "a string"

def read_doc(slug: str) -> str:
    if not SLUG.match(slug):
        raise ValueError("slug outside expected range")
    target = (DOC_ROOT / f"{slug}.md").resolve()
    if DOC_ROOT not in target.parents:   # context: must stay under the root
        raise ValueError("path escapes document root")
    return target.read_text()

The rule of thumb: if a parameter can change which file, host, or command the tool reaches, validate it against the set of things the tool is allowed to reach, not against its type. A type check confirms the shape. A context check confirms the intent, which is the part the attacker controls.

The runtime controls it names

Beyond the build-time rules, the sheet names specific runtime defenses, and they read like a checklist for the perimeter around an MCP deployment.

A filtering outgoing proxy or enterprise DLP for external MCP connections, with resource URLs and access methods pinned to cut unintended leakage. This is the control that catches the SSRF and exfiltration paths, because a server whose outbound requests must pass a filtering proxy cannot reach a cloud metadata endpoint or an attacker's collector. CloudSEK showed how one unvalidated fetch tool turned into AWS credential theft; a pinned egress path is what closes it at the network layer.

Sandboxing for MCP-enabled services, so a compromised server runs with a constrained view of the host rather than the developer's full account. The sandboxing MCP servers playbook covers the container and seccomp side of this.

Output filtering and local MCP scans round out the list. Filter what the server returns so a tool result cannot smuggle a follow-on instruction back into the model, and scan the servers you install before you trust them.

Message integrity ties the runtime list back to the inversion problem. The sheet wants you to bind each request to a time and a context so a captured message cannot be replayed or run out of order. In practice that is an expiration timestamp the server checks, a nonce it remembers for the life of the session, and an optional signature over the payload. None of that is in the base protocol, so it lands on you, and the cost is a few fields and a check. The payoff is that the funds-transfer message from the replay example fails the second time because its timestamp is stale and its nonce is spent.

The sheet frames all of this as a continuum: these are not isolated problems you patch at one interface. A misaligned assumption at any stage propagates and compounds, so you secure the whole agentic environment or you secure none of it. That framing is the reason the document lists controls at the build layer, the data layer, and the network layer instead of picking one. An MCP server with perfect input validation and no egress filtering still leaks through the proxy you forgot to put in front of it.

What this means for your stack

Pick the two cheapest controls and ship them first. Turn on structured logging of every tool call, with parameters and the calling identity, and route it to wherever your other security logs go. Then split your MCP servers into a public-data profile and a sensitive-data profile, and never start both in the same agent session. Neither change requires touching the protocol, and together they cover the observability and segregation the CSI leans on hardest.

The architectural pattern under the whole sheet is least privilege plus a record at the point of action. The credential an MCP server holds should be scoped to one job and short-lived, so a session hijack or an SSRF borrows something that expires. Every action the server takes should leave a line you can replay during an incident. Build those two properties and most of the CSI's recommendations fall out of the design instead of being bolted on after a finding.

hasp is one working implementation of that pattern. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, and an MCP server receives scoped, time-limited references in place of standing keys, with every request recorded as it happens. Source-available (FCL-1.0), local-first, macOS and Linux, no account.

The sheet is worth reading in full even if you adopt none of its tooling, because it is the clearest statement yet of what changed when servers started acting for clients. The old controls still apply. They stopped being enough, and an agency with no stake in the outcome put that in writing.

Sources· cited above, in one place

NEXT STEP~90 seconds

Stop handing the agent your real keys.

hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.

  • Local, encrypted vault — no account, no cloud, no telemetry by default.
  • Brokered run — agent gets a reference, the child process gets the value.
  • Pre-commit + pre-push hooks catch managed values before they ship.
  • Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
→ okvault unlocked · binding ./api
→ okgrant once · pid 88421
→ okagent never read

macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.

Browse all clusters· eight threads, one index