Nick George — AI Agents & Automation
Open source · mcp-gatehouse

Decide what an AI can touch. Keep a record of everything it did.

mcp-gatehouse is a small Python library for MCP servers, the layer that lets an AI work inside your business systems. You label each tool by what it could break. Lookups run on their own. Anything you can't take back waits for a person to say yes. And every call, allowed or refused, lands in a log you can still read six months from now.

pip install mcp-gatehouse

Open sourceMITPython 3.10+MCP SDK v2v0.3.1

Nick George, Charlotte, NC

Why I built it

An AI that can read an order can usually cancel one too.

I build and run MCP servers for a mid-market reverse logistics-tech company. More than 50 people use them every day to look up inventory, move orders and check truckloads.

Wiring the AI in was quick. Deciding what it could do once it got there was most of the work, because when you're moving real inventory, a confident wrong move is expensive. Get that wrong and you've handed a very fast intern the keys to every room.

Most MCP servers I come across skip that step. They hand the model every tool at full strength and keep no record of what it did. That's fine for a demo. It stops being fine the day an agent can write to your CRM, your books or your order system.

So every server I build has the same gate in it. mcp-gatehouse is that gate, pulled out of my production work into a library you can drop into your own server.

I had a second reason, too. If you're thinking about letting me connect AI to your business, your technical people should be able to read how I keep it in bounds before we talk. The code is public, tests included.

Nick George · Charlotte, NC

How it works

Every tool call goes through the gate first.

You build your MCP server the way the SDK docs show. The one change is that you register each tool through the gatehouse and give it a tier: READ for lookups, WRITE for creating or updating records, DESTRUCTIVE for deleting, sending or paying, the things that are hard or impossible to take back. From then on, every call takes the same path.

  1. 01

    Is the tool blocked?

    If its name is on your denylist, the call stops here, whatever its tier.

  2. 02

    Does its tier need a yes?

    By default only DESTRUCTIVE does. One line adds WRITE. The yes comes from an approver: the terminal prompt that ships with the library, or a function you write that asks in Slack or opens a ticket.

  3. 03

    No approver, no call.

    If the tier needs a yes and there's no approver set up, the call is refused. If the approver itself breaks, say Slack is down, the call is refused too.

  4. 04

    Run it, or say why not.

    Allowed calls run as normal. A refused call tells the model the reason, so it can pass that on to the person it's working for, and your server doesn't log it as a crash.

  5. 05

    Write it down.

    One line in the audit log either way: the tool, its tier, the arguments with secrets masked, the outcome, and how long it took.

What the log looks like

A lookup, a note with its API key masked, and a cancel the approver turned down. This is what you pull up when someone asks what the AI did.

{"ts": "2026-07-16T14:02:11+00:00", "tool": "lookup_order", "tier": "read", "outcome": "ok", "arguments": {"order_id": "4417"}, "duration_ms": 0.42}
{"ts": "2026-07-16T14:02:38+00:00", "tool": "add_note", "tier": "write", "outcome": "ok", "arguments": {"order_id": "4417", "note": "call back", "api_key": "«redacted»"}, "duration_ms": 1.08}
{"ts": "2026-07-16T14:03:05+00:00", "tool": "cancel_order", "tier": "destructive", "outcome": "denied", "reason": "approver refused", "arguments": {"order_id": "4417"}}

What you get

What's in the library.

Permission tiers

Every tool is READ, WRITE or DESTRUCTIVE. The tier also sets the readOnlyHint and destructiveHint annotations that MCP clients read, and the wrapper won't let you override them to claim something the tier doesn't.

Approval gates

Pick the tiers that need a sign-off. An approver is any function, sync or async, that gets the request and returns True to allow it. A sync approver runs in a worker thread, so a person taking their time doesn't hold up everyone else's calls.

An audit log you can read

Append-only JSONL, one line per call, allowed, refused or failed, with a UTC timestamp and a duration.

Secrets stay out of it

Arguments named api_key, password, token, client_secret and a dozen more are masked before they reach the log or the approver, however they're spelled. apiKey and API-Key both count. Error messages are left out too, and only the error's type is kept, because error text loves to repeat the very value you just masked.

A denylist

Switch a tool off by name, whatever its tier, without deleting its code.

Nothing else to run

No proxy, no policy service, and no dependencies beyond the official MCP Python SDK. It's a few small classes and a JSONL file.

The code

Swap one decorator and the gate is in.

Build your MCPServer as usual, then register tools with @gatehouse.tool instead of the server's own decorator. That's the whole integration.

from mcp.server.mcpserver import MCPServer
from mcp_gatehouse import AccessTier, AuditLog, Gatehouse, Policy, terminal_approver

mcp = MCPServer("order-desk")
gatehouse = Gatehouse(
    mcp,
    policy=Policy(approver=terminal_approver),
    audit=AuditLog(path="~/order-desk/audit.jsonl"),
)

@gatehouse.tool(tier=AccessTier.READ)
def lookup_order(order_id: str) -> str:
    """Look up an order's status."""
    ...

@gatehouse.tool(tier=AccessTier.DESTRUCTIVE)
def cancel_order(order_id: str) -> str:
    """Cancel an order. Runs only if the approver says yes."""
    ...

mcp.run()

Your tools keep their signatures, so schema generation, parameter descriptions and transports all work the way they did before. Sync tools still run in a worker thread, the same as on a plain MCPServer.

To make writes wait for a yes as well, it's one line: Policy(require_approval=frozenset({AccessTier.WRITE, AccessTier.DESTRUCTIVE})). To mask a key of your own without losing the defaults: Policy(redact=DEFAULT_REDACT | {"card_pin"}).

The full README on GitHub ↗

Try it

A demo order desk, with the gate closed.

The package comes with a small order-desk server: four tools across all three tiers. Add it to Claude Desktop, or any MCP client that runs servers over stdio. For Claude Desktop, it goes in claude_desktop_config.json:

{
  "mcpServers": {
    "gatehouse-demo": {
      "command": "uvx",
      "args": ["mcp-gatehouse", "--audit-log", "/tmp/gatehouse-audit.jsonl"]
    }
  }
}

Ask the model to list the orders, then cancel one. The cancel needs a yes typed into a terminal. A desktop client starts the server in the background, with no terminal to ask, so the cancel gets refused. That's the gate failing closed.

To approve one yourself, run the server from a terminal (mcp-gatehouse-demo, after pip install mcp-gatehouse) so the prompt has somewhere to appear. Either way, the verdict ends up in the audit log, and the server prints the log's path when it starts.

Design choices

Why the gate lives inside your server.

A proxy in front of your server can't see what your tools do. A separate policy service is one more thing to deploy and keep running, and a 40-person plant doesn't have a platform team to run it. The place that knows a lookup from a cancel is the tool itself, so that's where the tier is declared and where the gate is enforced.

It fails closed everywhere it can, because a security default that quietly allows is worse than none. A missing approver means no. An approver that crashes means no. A value the redactor can't take apart is swapped for a placeholder instead of going into the log as it is.

What it isn't

  • Authentication. It doesn't check who is connecting to your server.
  • Transport encryption.
  • A sandbox for your tool code.
  • Protection from a server author with bad intentions. Give a third-party MCP server the same scrutiny as any software you install.

It's a gate inside your server, for what an AI that's already connected is allowed to do. If you find a security problem, email nick@nickgeorgeai.com instead of opening a public issue. You'll hear back within 72 hours.

Compatibility

SDK v2 today, v1 still covered.

Which mcp-gatehouse versions work with which MCP Python SDK
mcp-gatehouseMCP Python SDKServer class
0.3.xmcp>=2,<3MCPServer
0.2.xmcp>=2,<3MCPServer
0.1.xmcp>=1.27,<2FastMCP

Tested in CI on every Python from 3.10 to 3.14. The public API (Gatehouse, Policy, AuditLog, AccessTier) hasn't changed across those versions, as promised when SDK v2 was still on its way. Porting a v1 server is two import edits. Staying on v1 takes no action at all: 0.1.x pins mcp<2, and it still gets security fixes.

mcp-gatehouse on PyPI ↗Release notes ↗

Common questions

Questions about mcp-gatehouse.

Is mcp-gatehouse free?

Yes. It's open source under the MIT license. Install it with pip install mcp-gatehouse. It needs Python 3.10 or newer and the official MCP Python SDK, and nothing else.

Does it work with FastMCP or version 1 of the MCP Python SDK?

Yes, through the 0.1.x line, which targets mcp>=1.27,<2 and FastMCP. Version 0.2 and later target SDK v2 and its MCPServer class. The public API is the same on both, and porting a v1 server is two import edits.

Can approvals go to Slack or a ticket instead of a terminal?

Yes. An approver is any function, sync or async, that receives the request and returns True to allow it. The library ships a terminal approver; a Slack or ticket approver is a function you write. Either way, the approver sees the arguments with secrets already masked, and if it fails, the call is refused and logged.

What does the AI see when a call is refused?

The reason. A refusal is raised as GateDenied, which the MCP SDK treats as an expected tool error, so the message reaches the model and the server doesn't log a crash. The audit log gets a line with the outcome "denied" and why.

Does it handle authentication?

No. It doesn't authenticate clients, encrypt transports or sandbox tool code. It's a gate inside your server that decides what an AI that's already connected is allowed to do, and records what it did.

Why put the gate inside the server instead of using an MCP proxy?

A proxy can't see what your tools do, and a separate policy service is one more system to deploy and keep running. Inside the server, each tool's tier is declared right where the tool is written, and enforced at the moment it's called.

Is it on the official MCP Registry?

Yes, as io.github.nickgeorgeseo/gatehouse, installed from PyPI. That entry is the demo order-desk server, which runs with uvx mcp-gatehouse.

Testing an agent before it gets near real data? fieldtwin, my open-source Jobber and QuickBooks twins →

Want this around your own systems?

I build MCP servers with the gate already in.

If you want AI working in your CRM, your books or your order system, and you want to know what it's allowed to do there, that's the work I do.

MCP server development →

New to MCP? The plain-English guide →