DEV Community

Umang Kumar
Umang Kumar

Posted on Originally published at cirvix.com

MCP Security: Where to Put the Authorization Boundary

An MCP tool call passes through the model, the client, possibly a proxy, and the server before anything happens. Only one of those places sees every call in structured form and is controlled by you rather than by the agent or a third party: the seam between client and servers. That is where an MCP security gateway belongs, and this page shows how to put one there.

One seam: client ↔ servers

server__tool: namespaced per upstream

Default deny: unlisted tools refused

Hash-chained: audit per decision

The question

Five places you could enforce. One that works.

Every MCP security control is really a decision about where the authorization boundary sits.

The MCP security overview covers what goes wrong: broad-scope tools, poisoned tool descriptions, credentials leaking through results. This page answers the follow-up question teams ask once they accept that per-call policy is needed: where should the decision be made?

Where the boundary sits What it sees Who controls it Verdict
In the prompt or system message Text The model, which can be talked out of it Advisory at best
Inside each MCP server Its own calls only Whoever wrote that server, often a third party Inconsistent across servers, no shared context
Network or HTTP proxy Hosts and bytes; nothing for stdio servers You Misses local servers and argument meaning
Gateway at the client–server seam Every routed tools/call: server, tool, arguments You, independent of agent and server The authorization boundary
In-process wrapper (guard.wrap) Calls to tools you own You Use for non-MCP tools alongside the gateway

The prompt is the wrong place because the same channel that carries policy also carries injected instructions. The server is the wrong place because you will run servers you did not write, each with its own idea of safety, and none of them knows what the agent did on another server a moment ago. A network proxy is the wrong place because most local MCP servers speak stdio and never open a socket, and a proxy sees a hostname, not that the tool is drop_table.

The client–server seam has none of these problems. Every tools/call crosses it as structured JSON-RPC with a server, a tool name and arguments. It sits outside the model, so nothing the model reads can rewrite it. And it is one place, so one policy governs every server behind it.

Why the seam

What a gateway at the seam can do that nothing else can.

From the Cirvix gateway's documented behavior and its public source.

01

Evaluate before forwarding

Each tools/call is decoded, evaluated against policy, recorded and then forwarded, refused or held. Every governed agent action routed through Cirvix is evaluated before execution; a refused call never reaches the upstream server.

02

Keep servers apart

Tool names are namespaced as server__tool. Two servers that both expose search stay distinct, so a rule written for one cannot silently govern the other. Rules read the server and tool as mcp.server and mcp.tool.

03

Pin tool definitions

A tool description enters the model's context as instruction text supplied by the server. The gateway fingerprints each definition when it first sees it and withholds a tool whose definition later drifts from that pin.

04

Return denials the agent can read

A refusal comes back as a tool result with the rule, reason and remediation, not as a transport error. The agent can re-plan instead of treating the server as broken.

05

Record one chain for every server

Decisions from every upstream land in the same SHA-256 hash-chained audit log, and results are scanned on the way back.

Setup

The artifact: a gateway in front of two servers.

Three files. Upstreams in one, the client's gateway-only map in another, policy in the third.

mcp-upstreams.json — what the gateway connects tojson

{
  "mcpServers": {
    "github":   { "command": "npx", "args": ["-y", "<your-github-mcp-server>"],
                  "env": { "GITHUB_TOKEN": "<token>" } },
    "postgres": { "command": "npx", "args": ["-y", "<your-postgres-mcp-server>", "<connection-string>"] }
  }
}
Enter fullscreen mode Exit fullscreen mode

.mcp.json (or your client's MCP config) — the only server the client seesjson

{
  "mcpServers": {
    "cirvix": {
      "command": "cirvix",
      "args": ["gateway",
               "--servers", "/abs/path/mcp-upstreams.json",
               "--policy",  "/abs/path/cirvix.policy.json",
               "--cwd",     "/abs/path/workspace"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

cirvix.policy.json — per-server, per-tool rulesjson

{
  "rules": [
    { "name": "hold-github-writes", "effect": "hold",
      "when": [
        { "path": "mcp.server", "op": "eq", "value": "github" },
        { "path": "mcp.tool",   "op": "in", "value": ["create_pull_request", "merge_pull_request", "push_files"] }
      ],
      "approvers": ["maintainers"],
      "reason": "Writes to GitHub need a maintainer." },

    { "name": "allow-github-reads", "effect": "permit",
      "when": [
        { "path": "mcp.server", "op": "eq", "value": "github" },
        { "path": "mcp.tool",   "op": "in", "value": ["get_file_contents", "search_issues", "list_issues"] }
      ] },

    { "name": "allow-postgres-query", "effect": "permit",
      "when": [
        { "path": "mcp.server", "op": "eq", "value": "postgres" },
        { "path": "mcp.tool",   "op": "eq", "value": "query" }
      ] }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The policy names tools explicitly with mcp.tool rather than relying on action classes like fs.write. Cirvix classifies tool names into actions heuristically, which is useful for generic rules, but for third-party MCP tools an explicit tool list is clearer to review and cannot be surprised by a name the classifier reads differently. After your first real calls, check cirvix logs to see exactly how each tool was normalized.

Walkthrough

Four calls through the gateway.

Output from @cirvix_ai/agent-control 0.2.5 with the policy above, against stub upstream servers named postgres and github. Responses trimmed.

JSON-RPC transcripttext

# client -> gateway: tools/call  postgres__query  {"sql":"select count(*) from orders"}
<- result: "upstream ran query ..."                       # PERMIT  allow-postgres-query

# client -> gateway: tools/call  postgres__drop_table  {"table":"orders"}
<- isError: true
   "Denied by policy: default-deny
    No rule permits this call. The policy set is default-deny: an action must be explicitly allowed."
   _meta: { "cirvix/verdict": "deny", "cirvix/rule": null, "cirvix/decision_id": "dec_..." }

# client -> gateway: tools/call  github__create_pull_request  {"title":"Bump deps"}
<- isError: true
   "Held for human approval: hold-github-writes
    Writes to GitHub need a maintainer.
    Waiting on: maintainers"
   _meta: { "cirvix/verdict": "hold", "cirvix/rule": "hold-github-writes", "cirvix/approval_id": "apr_..." }

# client -> gateway: tools/call  github__get_file_contents  {"path":"README.md"}
<- result: "upstream ran get_file_contents ..."           # PERMIT  allow-github-reads

$ cirvix audit verify      # Hash chain intact · 4 records verified
$ cirvix approvals         # 1 call waiting: create_pull_request, policy hold-github-writes
Enter fullscreen mode Exit fullscreen mode

The read query runs because allow-postgres-query names it. drop_table is refused with rule: null: nobody wrote a rule forbidding it, and nobody had to, because nothing permits it. The pull request is held for maintainers and shows up in cirvix approvals; approving records a single-use grant for a retry, and nothing resumes on its own. The file read passes. Four calls, four decisions, one verified chain.

Limits

What the gateway does not cover.

Be exact about this in your threat model.

01

Calls that don't route through it

Direct upstream entries left in a client config, editor built-in tools, unwrapped callables and arbitrary subprocesses are outside the boundary.

02

General network egress

The gateway speaks MCP. Setting HTTP_PROXY does not route ordinary agent traffic through its policy engine.

03

Authentication

It does not replace MCP authentication or the server's own credential checks. It adds a per-call decision on top of them.

04

Prompt injection itself

It does not detect injected instructions. It limits what an injected agent can get the servers to do. The AI agent authorization walkthrough shows the same rules applied to non-MCP calls.

05

Live policy reload

Rules are chosen when the gateway starts. Plan a controlled restart after a policy change.

Deployment checklist

Putting an MCP security gateway in front of your servers.

Ten steps. The full command reference is in the MCP guide.

01

Inventory your MCP servers

Run npx @cirvix_ai/agent-control scan. It reads known client configs and flags mcp-broad-scope, mcp-inline-secrets and mcp-duplicated. A server with filesystem scope / is the first one to put behind policy; see broad filesystem scope.

02

Move upstreams into their own file

Copy the server entries into mcp-upstreams.json. The gateway accepts mcpServers, servers or a bare map, with command/args/env for stdio or url/headers for HTTP upstreams. Never point the gateway at the client's own gateway-only file.

03

Install the CLI globally

npm install -g @cirvix_ai/agent-control. A project-local install is generally not on your editor's PATH, so the client could not launch cirvix.

04

Make the gateway the only server the client launches

Replace the client's server map with the single cirvix entry and absolute paths. Any direct upstream entry left behind is an ungoverned route.

05

Write rules per server and per tool

Scope permits with mcp.server and mcp.tool. Unlisted tools on listed servers, and every tool on unlisted servers, fall through to default deny.

06

Hold the writes that matter

Merges, pushes, migrations, payments: use hold with named approvers rather than a blanket permit or a blanket deny.

07

Validate and test before restarting

Run cirvix policy check and cirvix policy test. Gateway rules are loaded once at startup, so restart the client deliberately after a policy change.

08

Prove routing with real calls

From the client, make one call you expect to pass and one you expect to be refused. Then cirvix logs and cirvix audit verify: expect both decision IDs and a non-zero record count. Starting the gateway is not proof that the client uses it.

09

Keep HTTP mode on loopback

cirvix gateway --http --host 127.0.0.1 --port 8787 serves Streamable HTTP. Do not expose an unauthenticated gateway on a public interface; non-loopback use needs a token and a reviewed TLS and network boundary.

10

Cover what the gateway can't see

Built-in editor tools and subprocesses are outside the boundary. Restrict them with host and editor permissions, and wrap any non-MCP tools you own with guard.wrap.

FAQ

MCP security gateways, asked directly.

Short answers.

01

What is an MCP security gateway?

A process that sits between an MCP client and its upstream MCP servers, presents itself to the client as a single server, and evaluates each tools/call against policy before forwarding it. Cirvix runs one locally with cirvix gateway.

02

Is an MCP gateway the same as MCP authentication or OAuth?

No. Authentication establishes who is on each side of a connection and which credentials a server accepts. A gateway decides whether each specific call may execute. You want both: authentication on the connection, authorization on every call.

03

Can I just use an HTTP proxy for MCP security?

Not for stdio servers, which never touch the network, and not for decisions about tool arguments, which a network proxy sees only as bytes. The Cirvix gateway speaks MCP; it is not an HTTP_PROXY egress proxy and does not govern ordinary agent HTTP traffic.

04

Does the gateway protect editor built-in tools?

No. Every governed agent action routed through Cirvix is evaluated before execution, and built-in editor tools, direct upstream entries and arbitrary subprocesses do not route through the gateway. Use host permissions for those, and guard.wrap for tools you own.

05

How do I know my client is actually using the gateway?

Remove every direct upstream entry from the client config, generate a benign call and a call you expect to be denied, then run cirvix logs and cirvix audit verify. You should see both decision IDs and a non-zero record count.

Next step

Put one MCP server behind the gateway.

Start with the server that has the broadest scope, watch the first deny land, then expand. The MCP guide walks through install, connect, policy, test and verify.

Read the MCP guide →Runtime authorization guideMCP security overview


Originally published on cirvix.com. Cirvix is open source: try it locally with npx @cirvix_ai/agent-control scan or see github.com/CIRVIX/agent-control.

Top comments (0)