PERMISYN
MCP gateway

Ten checks on every tool call. Then the tool runs.

permisyn-mcp stands where your MCP server stands. Every tools/call is held, checked against the same signed passport that governs the agent's model calls, and leaves a receipt anyone can verify — whether it ran or was refused.

Start free Both surfaces, side by side
tools/callauthorizeyour servercomplete
Where it sits

In front of the MCP servers you already run.

Nothing about your servers changes and nothing about your client changes. One process moves into the middle of a connection that already exists, and it is the only thing that has to be trusted with the decision.

Your MCP client
Claude Desktop, Cursor, Claude Code, your own agent
permisyn-mcp10 checks
runs in place of your server, holds every tools/call for an authorize
Your MCP server
a spawned subprocess, or a hosted Streamable HTTP endpoint

Refused at the middle step, the real server never sees the call and there is nothing to undo. This is the difference between governing a tool call and auditing one after the file was already written.

01It relays JSON-RPC, it does not replace it
Your client talks to permisyn-mcp, which talks to your real server. The handshake, tools/list, resources, prompts, notifications and pings pass straight through untouched. Two messages are held: a tools/call going out, and a sampling/createMessage coming back.
02The tool list is hashed on the way past
A tool's description is read by your model as instructions and its schema decides which arguments are legal — both owned by whoever runs the server, and changeable after you reviewed them. Each definition in a tools/list answer is hashed as it goes by. The list itself still reaches your client byte for byte.
03A changed definition waits for a person
The first definition seen for a tool is the baseline. Anything different after that is recorded as a change, alerted on, and shown as a diff to accept or refuse. In pin mode the tool does not run until somebody decides.
04Held, then authorized, then run
The call stops at the gateway while one HTTPS request asks the API for a decision. Allowed, it is forwarded to your server unchanged and the result goes back to your client unchanged. Refused, your server never hears about it and the client gets a tool error naming the rule.
05It fails closed, not open
If the authorizing API is unreachable, slow past fifteen seconds, or answers with anything other than a decision, the tool call is refused. A governance layer that passes calls through when it cannot reach its own policy is not one.
06Two transports, one rule about plaintext
A local server spawned as a subprocess, or a hosted Streamable HTTP endpoint given by URL. Plaintext http is accepted only to localhost — everything forwarded is a call that was just approved, with your credentials attached.
07Your server data does not pass through us
The gateway runs on your machine. What reaches Permisyn is the authorize request and the completion record, not the traffic between the gateway and your server.
08The tool result is summarised, not collected
Completion carries the first 2,000 characters of the result as JSON, scrubbed for secrets before it is stored. A receipt proves what was decided and what happened, not the contents of your files.
Every tool call, in order

What runs before your server is asked to do anything.

Broadest scope first, so a refusal names the most specific rule that applies rather than a generic denial. The order is the router's own, and the value beside each row is what lands on the receipt.

  1. 01Plan limits and rateThe same per-minute rate limit and monthly quota an LLM call is bound by, calling the same functions. A tool call is not a separate unmetered surface.mcp_plan_rate_limited
  2. 02Kill switchOne switch stops an agent on both surfaces at once. Nothing about pulling it is MCP-specific, which is the point of it being the same agent.mcp_kill_switch_active
  3. 03Org, team or sponsor freezeBroadest scope first: a frozen org, then a halted team, then a halted human sponsor. The receipt names which of the three applied.mcp_org_frozen
  4. 04Passport expiryAn expired passport authorises nothing. Expiry is a date on the signed passport, not a background job that might not have run.mcp_passport_expired
  5. 05Active hoursAn agent allowed to run weekdays 09:00–18:00 is refused outside them, in the timezone the passport names.mcp_passport_time_window
  6. 06Allowed toolsThe tool has to be on this agent's allow-list — the same list that governs the tools it may ask a model for, not a second one kept in step by hand.mcp_action_forbidden
  7. 07Delegation grantWhen this call is running under a grant handed down by another agent, the tool must also be inside that grant. A grant can only ever narrow.mcp_delegation_forbidden
  8. 08Argument policyread_file allowed in general, but only under one path. A rule the gate cannot parse fails closed rather than reading as a check that passed.mcp_argument_forbidden
  9. 09Tool definitionIn pin mode, a tool advertising a description or schema nobody accepted does not run. Every other gate here asks about the caller; this one asks whether the tool is still the tool that was reviewed.mcp_tool_definition_changed
  10. 10Model intentIn require mode, a tool call with no model decision behind it does not run. Last in the chain because it consumes that decision, and an earlier refusal must not spend it.mcp_intent_unmatched

Ten here against seventeen on a model call, because the two surfaces have different things to check — but the passport, the freezes and the kill switch behind both lists are the same objects.

The other direction

A tool cannot borrow your model without asking.

MCP lets a server send sampling/createMessage to your client: run this inference for me, on your model, on your bill, over a conversation I wrote. It is the one AI call in the protocol your agent never asked for, and the only message this gateway inspects before your client is allowed to see it.

your serverauthorize-samplingrefusedyour client never sees it

Six checks, not the ten above: an allow-list of tool names, an argument policy and a tool definition have nothing to say about a request for inference. What is left is every check that asks whether this agent may spend anything at all right now, plus the model the server named.

  1. 01Plan limits and rateA borrowed model call is a model call. It counts against the same per-minute rate and monthly quota, so a server cannot sample its way around a ceiling.mcp_plan_rate_limited
  2. 02Kill switchOne switch, three surfaces. A killed agent does not call a model, does not call a tool, and does not lend its model to a server that asks.mcp_kill_switch_active
  3. 03Org, team or sponsor freezeThe same freezes, in the same order. A frozen org's servers stop borrowing at the same moment its agents stop calling.mcp_org_frozen
  4. 04Passport expiryAn expired passport lends nothing. The date on the signed passport is what answers, not a job that may not have run.mcp_passport_expired
  5. 05Active hoursA borrow outside the agent's declared hours is refused for the same reason a tool call is. Nothing about being asked by a server extends the window.mcp_passport_time_window
  6. 06Model the server asked forThe model named in modelPreferences.hints has to be one this agent's passport allows, and one any delegation grant in play also allows. A grant can only narrow.mcp_sampling_model_forbidden
01It is inspected before your client sees it
The one message this gateway holds on the way IN. Every other reply from your server is forwarded first and recorded after, because recording must never slow a tool result down — but a borrow that reaches your client has already happened, and describing an ungoverned model call is not governing one.
02Refused to the server, invisible to the client
A refused borrow is answered back to the server as a JSON-RPC error. Your client is never shown the request at all, so there is nothing for it to be talked into.
03The conversation never leaves your machine
What Permisyn is told is the model the server named, how many messages there were, and whether a system prompt was present. Not their contents. There is no field the messages could travel in, which is why a borrow can be authorized without anybody reading it.
04It starts in observe, and observe delivers
A server that samples today keeps working the day the gateway is installed in front of it. Observe records every borrow and refuses none — and records what require would have refused, so switching is a decision rather than a leap.
05The allowed borrow's receipt cannot be lost
Unlike a tool call, whose allowed receipt is written after the tool returns, a borrow is recorded at the moment it is decided. There is no second call to fail.
06Fails closed, like everything else
An unreachable API, a non-200, or an answer that is not a decision refuses the borrow. A server does not get your model because our control plane was down.
The install

One server by hand, or every server on the machine.

The authorizing API serves its own client, so the install is one curl — or run it with npx or uvx and install nothing at all. What you paste below is generated by the same code that generates it inside the product.

01Install the gateway
curl -fsSL https://api.permisyn.com/api/mcp/v1/client -o /usr/local/bin/permisyn-mcp && chmod +x /usr/local/bin/permisyn-mcp
02Point one server at it
{
  "mcpServers": {
    "your-server-name": {
      "command": "/usr/local/bin/permisyn-mcp",
      "args": ["--agent", "fs-agent", "--",
                "npx", "-y", "@modelcontextprotocol/server-filesystem", "/path"],
      "env": {
        "PERMISYN_API_KEY": "$PERMISYN_API_KEY",
        "PERMISYN_USER": "you@company.com",
        "PERMISYN_TEAM": "platform"
      }
    }
  }
}
03Or govern every server already configured
npx -y @permisyn/mcp wrap --dry-run
npx -y @permisyn/mcp wrap --agent fs-agent

/usr/local/bin/permisyn-mcpis absolute and on the default system path on purpose: desktop MCP clients are started by the operating system rather than by your shell, and a correct config that relies on your shell's path is the most common way this fails.

01Wrapping is reversible and never doubles
Wrap edits the entries in place after taking a backup, and unwrap puts them back. An already-wrapped server is left alone rather than wrapped twice, so running it again is safe.
02It knows where the configs live
Claude Desktop, Cursor (global and per project), VS Code workspaces, Windsurf, and Claude Code project files. Run it with --dry-run first and it prints what it would change without touching anything.
03A legacy SSE server is skipped, and says so
The old HTTP+SSE transport is a different wire protocol, and wrapping it would break a working server. Those entries are reported as skipped and left ungoverned rather than quietly half-converted.
04doctor tells you what is not governed
It checks the API is reachable with your key and lists which of your MCP servers are behind the gateway and which are not — the question that actually matters after an install.
05The identity rides along
The team and human sponsor in the config land on every receipt the agent produces, which is what makes a tool call attributable to a person rather than to a machine.
06Hosted servers use static headers
An auth header can be passed through to a hosted endpoint. Interactive OAuth against a remote MCP server is not implemented, and no amount of configuration makes it work today.
What it leaves behind

A signed receipt for the call, and for the refusal.

The same evidence format the LLM side produces, from the same signing key, verifiable by someone who has never heard of your account. Governance you cannot show someone else is just configuration.

01An Ed25519 signature over the whole record
The agent, the tool, the server, the decision, the timing, the redacted arguments and result — signed as canonical JSON. Tampering with any signed field breaks verification.
02Verifiable by someone with no account
Every run has a public page and a public endpoint. Hand an auditor a verify link and they check the signature against the published key themselves, without asking us for anything.
03A refusal is evidence too
A blocked tool call is recorded with the rule that blocked it, at the moment it is blocked. "Nothing happened" is a claim, and this is what makes it checkable.
04Secrets are scrubbed before storage
Arguments and the result summary are both run through the same redaction the LLM receipts use, so a token passed as a tool argument does not become a permanent record.
05Optional proof of the output
Turn on output attestation and the completion carries a SHA-256 of the result summary, binding the receipt to what the tool actually returned rather than only to the fact it ran.
06Chains without threading an id
Give the gateway a chain id and its receipts link into that chain. Or do not: a tool call matched to a model decision picks up the chain from that decision by itself.
07Which definition of the tool ran
Every receipt carries the digest of the definition the tool was advertising at the time, and whether anybody had accepted it. Months later that is the difference between "read_file ran" and "the read_file we reviewed ran".
08A definition refusal names both digests
When a pinned tool is refused, the receipt carries the digest it is advertising now and the one it displaced. Two hashes on one signed record, so the change is checkable without trusting the screen that showed it to you.
Both surfaces

Bound to the model decision that asked for it.

A tool call is matched to the model decision that asked for it, so a receipt names the prompt behind an execution — and a call no governed model asked for can be refused.

When a governed model call decides to use a tool, that decision is recorded. The gateway matches an arriving tool call against it and consumes it, so one decision authorises exactly one execution — and the receipt names the run that asked. The chain from prompt to tool call then appears on the public verifier with nothing configured.

Needs both halves: an agent whose model traffic does not pass through Permisyn records no decisions to match. It starts off, records without refusing in observe, and only refuses in require. See it beside the LLM surface or read how the match is made and recorded.

Stated plainly

What the gateway does not do.

Eleven things, several of which are the kind usually left for a customer to discover. A limit you publish is worth more than a claim you have to defend later.

01Two of the protocol's messages are gated
A tools/call from your client, and a sampling/createMessage from your server. Everything else relays untouched — the handshake, tool listings, resources, prompts, elicitation, notifications — and leaves no receipt either.
02A borrow's outcome is not recorded
The receipt says which model a server asked for, not which one your client then chose or what it spent. The gateway answers before your client sees the request and never hears the reply, and the tokens are billed to your own model account, which Permisyn does not read.
03The model preference is a hint, by specification
MCP says modelPreferences is advisory: your client picks. So refusing a model here is a real gate on what the server ASKED for, and no gate at all on what your client does with an allowed one.
04Sampling over a hosted server is untested
Server-initiated requests arrive on the stdio transport. A hosted Streamable HTTP endpoint has no channel to send one on today, so this lands where the traffic is.
05An allowed call's receipt is written after it runs
The decision is synchronous; the record of what happened comes from a second, best-effort call once the tool returns. If that call cannot be delivered at all, the tool has still run correctly and that one receipt is missing. Taking a long time is not one of the ways it can be lost — a completion is accepted for an hour, and one arriving after the first five minutes says so on the receipt. A refusal is recorded synchronously and cannot be lost either way.
06No cost on a tool call
Cost caps, the waste ledger and the spend controls are proxy-side, because a tool call has no provider invoice to read. MCP receipts carry zero cost, honestly rather than as an estimate.
07No provider or region checks
Those pin down where a model call is sent, and a tool call is not sent to a provider. The model allow-list does bite on one thing here — a server asking to borrow that model — and on nothing else.
08The result hash covers the summary
Output attestation hashes the 2,000-character summary, so for a larger result it binds the receipt to that summary rather than to the full output. It is a real hash of a real thing, and the thing is stated.
09Requiring model intent needs the other surface
Refusing tool calls no governed model asked for only works if your model traffic goes through Permisyn too. For an agent whose model calls go direct, require would refuse everything, which is why the product will not let you switch it on until decisions have actually been recorded.
10Pinning needs a gateway that reports
Definitions are pinned from what the gateway sees, so a server nothing has connected to through permisyn-mcp has nothing on record — and pin will not switch on for a workspace in that state. A definition change and a call in the same instant is caught within a few seconds of the change, not necessarily on the very next call.
11The diff can be shortened, the hash cannot
Descriptions and schemas are stored capped for display, so a very long one is shown in part and says so. What is hashed and compared is always the whole definition. Two servers advertising the same tool name is reported as a signal, not refused — two servers legitimately having their own search is ordinary.

Put it in front of one server and watch what it refuses.

The free plan includes the gateway, real receipts and the public verifier. Install it against a filesystem server, take a tool off the allow-list, and read the record of the call that did not happen.