Skip to content

Security model

An MCP server hands a model the ability to run things. The module's design assumes the model, the client and the network are all untrusted, and that the host is the only party whose word counts.

Authorisation is the host's, and it is asked every time

The registry has no notion of a user. It asks the host's Policy whether this caller may discover an operation and, separately, whether they may execute it with these exact arguments. Neither answer is cached across callers or across pages, and execution is asked even when inspection just succeeded. Denied and unknown names are indistinguishable to the caller. AllowRegistered exists for the local stdio case where the caller is the person who owns the binary; it is documented as unsuitable for HTTP and the module does nothing to make it otherwise.

Hints (read-only, destructive, idempotent, open-world) and presentation hints describe an operation to a client. They grant nothing, and a client acting on them is trusting the server's description, not the server enforcing anything.

Arguments cannot reach the shell, the executable or the environment

The Cobra binding builds argv, never a command line. Codec output is values for one named flag; the binding supplies the flag name, the command path and the -- separator, so a positional argument that looks like a flag stays a positional argument. The executable is resolved to an absolute path when the binding is created, the working directory and environment are fixed then too, and no tool argument can name any of them. A value the CLI parser would mis-read (a comma in a stringSlice element) is refused before a process starts rather than silently split.

Numbers survive exactly. The registry canonicalises JSON with decimal arithmetic, so a 64-bit identifier is never rounded through float64 somewhere between the client and the child; the binding renders integers in decimal from an exact rational.

Failures do not leak

A Failure carries a caller-safe code and message and a private cause. Only the safe parts are serialised; the cause is available to the host through the error chain and nowhere else. A handler panic, a policy error or an unconstructed result becomes a generic failure, not a stack trace. Resource issue reports name a URI only when the policy says the caller may see it, and never a hidden item.

Output is bounded

Every budget is fixed or configurable and enforced: request size, result size, inline media, content block count, retained subprocess output, continuation ledger, resource bundle. Overflow is a typed failure (result_too_large, budget_exceeded) or an explicit truncation flag, never a silently shortened JSON document.

Browser origins

The Streamable HTTP handler applies the standard library's cross-origin protection: a request with no Origin is a non-browser client and passes; a browser origin passes only when it is the server's own or listed in WithTrustedOrigins; anything else is refused with 403 before the protocol sees it. This closes the DNS-rebinding hole MCP's specification warns about. Authentication is still the host's middleware.

Continuations

A confirmation token is 256 random bits referencing a private ledger; the application's state never travels in the token. It is bound to the operation, the canonical arguments, the caller's scope key and the server's lifetime, and expires after ten minutes. A different caller, a changed answer or a restarted server cannot resume it. Completed work replays into a recovery-only handler call, so the module never re-executes a side effect on the strength of a retried token.

What the module does not do

It does not authenticate anyone, does not sandbox a child beyond the process group or job object it is started in, does not decode media to certify it, and does not verify that an external URL flow completed. Each of those is the host's, and the reference pages say so at the point where it matters.