mcp-gatehouse
This is an order-desk MCP server (the mcp-gatehouse demo) that lets an AI read and modify orders under permission tiers, approval gating, and audit logging.
List orders (
list_orders, READ) — see every order on the desk asid · customer · status, one per line, to find IDs for the other tools.Look up an order (
lookup_order, READ) — fetch one order's customer, status and notes by ID; returnsno order <id>if it doesn't exist.Add a note (
add_note, WRITE) — append a note to an order without approval (existing notes are kept, nothing is overwritten); an optionalapi_keyargument is masked in the audit log.Cancel an order (
cancel_order, DESTRUCTIVE) — irreversible; requires a human to approve on the server's terminal, otherwise the call is denied after refusal, a two-minute timeout, or no terminal available.Full auditing — every call (allowed, denied, failed, or cancelled) is written to an append-only JSONL log with UTC timestamps and durations.
Fail-closed security — gated tools are denied when no approver is available, and secret-bearing arguments are redacted before logging or approval.
mcp-gatehouse
Project page & docs: nickgeorgeai.com/open-source/mcp-gatehouse · PyPI
Permission tiers, approval gates, and audit logging for MCP servers. The server is the gatekeeper: you decide what an AI can read, what it can write, and what's off-limits — and every action gets logged.
Most MCP servers hand the model every tool at full strength and keep no
record of what it did. That's fine for a demo. It's not fine the day an
agent has write access to your CRM, your books, or your order system.
mcp-gatehouse is the missing gate, enforced inside the server — no
proxy, no external policy service, no dependencies beyond the official
mcp SDK.
pip install mcp-gatehouseWhat you get
Permission tiers | Every tool is declared |
Approval gates | Tiers you choose require a sign-off before the tool runs. Your approver is any callable — a terminal prompt, a Slack ping, a ticket. Fails closed: a gated tool with no approver configured is denied, not waved through. |
Audit log | Append-only JSONL, one line per call — allowed, denied, failed, or cancelled — with UTC timestamps and durations. The answer to "what did the AI actually do?" six months later. |
Redaction | Argument keys you name ( |
Denylist | Block a tool outright, whatever its tier. |
Related MCP server: TheeDiscordMCP
Quickstart
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()That's the whole integration: build your MCPServer exactly as the
SDK docs show, but register tools through the gatehouse. Schema generation
(including Annotated[..., Field(description=...)] parameter docs),
transports, and everything else work unchanged — the guard preserves the
function's signature. Sync tools still run in a worker thread, exactly as
they would on a bare MCPServer.
terminal_approver asks on the controlling terminal (/dev/tty), never
stdin — over stdio, stdin is the protocol pipe, so a plain input()
approver would corrupt it. An approver is any callable, sync or async,
that takes an ApprovalRequest and returns True to allow; sync ones run
in a worker thread from a pool of their own, so a human taking their time
doesn't stall other calls, and a pile of pending approvals can't take the
threads your sync tools need. If the approver raises (Slack down, ticket
API timing out), the call is denied and the failure is logged.
Give approvals a deadline so a sign-off nobody gives doesn't hang the call:
Policy(approver=terminal_approver, approval_timeout=120)Past it, the call is denied and logged as approval timed out. Approvers
see the deadline as request.deadline (a time.monotonic() value), and
terminal_approver withdraws its prompt when it passes. A sync approver
that ignores it finishes in the background and its answer is dropped. With
no timeout set, the gate waits as long as the approver takes.
Under the default policy, DESTRUCTIVE requires approval and everything
is audited. Gate writes too with one line:
Policy(require_approval=frozenset({AccessTier.WRITE, AccessTier.DESTRUCTIVE}), ...)Add your own secret-bearing keys without losing the defaults:
from mcp_gatehouse import DEFAULT_REDACT
Policy(redact=DEFAULT_REDACT | {"card_pin"}, ...)What the audit trail looks like:
{"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"}}
{"ts": "2026-07-16T14:05:40+00:00", "tool": "cancel_order", "tier": "destructive", "outcome": "cancelled", "reason": "awaiting approval", "arguments": {"order_id": "4418"}}outcome is ok, denied (reason says why), error (the tool
raised; reason is the exception type), or cancelled: the client gave
up, or the server shut down, before the call finished. reason says
when. awaiting approval means the tool never ran. tool running comes
with duration_ms, and the tool may have done part of its work. A sync
tool can't be stopped partway: its worker thread runs to the end, and the
line says ok or error as usual. The gate only writes the line; the
cancellation goes on as it would without it.
Try the demo
The package ships a runnable order-desk server with all three tiers wired
up and a terminal-prompt approver. Add it to any MCP client that speaks
stdio — for Claude Desktop, in claude_desktop_config.json:
{
"mcpServers": {
"gatehouse-demo": {
"command": "uvx",
"args": ["mcp-gatehouse", "--audit-log", "/tmp/gatehouse-audit.jsonl"]
}
}
}Or run mcp-gatehouse-demo yourself after pip install mcp-gatehouse.
Ask the model to list the orders and cancel one, then watch the verdict
land in the audit log either way. The audit log goes to --audit-log, else
$MCP_GATEHOUSE_AUDIT_LOG, else ./audit.jsonl — falling back to
~/.mcp-gatehouse/audit.jsonl when the working directory isn't writable
(desktop clients often launch servers from /). The server prints the
path it chose to stderr.
The approval prompt needs a terminal of its own: a client that launches
the server in the background has none, and a client with a full-screen
terminal UI is using its terminal, so in both cases cancel_order is
denied — the gate failing closed, as designed. To approve interactively,
start the client that launches the server from an ordinary terminal
session that isn't running a full-screen program. An unanswered prompt is
denied after two minutes. examples/orders_server.py is the same server as a copyable
template.
Design notes
Enforcement lives inside the server, at the tool boundary. A proxy can't see your tools' semantics, and a policy service is one more thing to deploy. A 40-person plant doesn't have a platform team; this is a few small classes and a JSONL file.
Fail closed. Security defaults that quietly allow are worse than none. That includes redaction: argument values the scrubber can't take apart (arbitrary objects, bytes) are replaced with an opaque placeholder rather than passed through, and exception messages stay out of the log — only the exception type is recorded, because error text loves to embed the very values you just redacted.
The audit log records denials, errors and cancellations, not just successes — the calls that didn't happen are half the story, and a call cut off halfway may still have done something.
A blocking terminal approver and the stdio transport don't mix — stdout/stdin are the protocol pipe.
terminal_approverprompts on/dev/ttyfor exactly that reason, and denies when there's no terminal or a full-screen program has it in raw mode. Real deployments should approve out-of-band: Slack, a ticket, a queue.What this is not: authentication, transport encryption, or a sandbox. It's a gate inside your server, not a perimeter around it. See SECURITY.md.
Compatibility
Targets the official mcp Python SDK
v2.x (mcp>=2,<3) and Python 3.10–3.14. Ships type information
(py.typed). Release notes: CHANGELOG.md.
| SDK | Server class |
|
|
|
|
|
|
|
|
|
|
|
|
The public API (Gatehouse, Policy, AuditLog, AccessTier) is
unchanged across that line, as promised. Porting a v1 server is two
import edits — FastMCP became MCPServer and moved to
mcp.server.mcpserver; see the SDK's
migration guide.
Staying on SDK v1 needs no action: 0.1.x pins mcp<2, so pip keeps
resolving it. That line is closed to features but still gets security
fixes.
Who built this
Nick George — I design and run MCP servers in production for a mid-market reverse logistics-tech company, and build them for businesses at nickgeorgeai.com. This library is the permission-and-audit discipline from those builds, extracted. Why I built it, and how it works in plain words, is on the project page.
If you're an owner or operator wondering what MCP even is, start with the plain-English guide: What is an MCP server?
License
Available Tools
4 toolsadd_noteA
Append a note to an order. Existing notes are kept — this never overwrites or cancels anything. Runs without approval, and every call is recorded in the audit log. To actually stop an order, use cancel_order.
| Name | Required | Description | Default |
|---|---|---|---|
| note | Yes | Free-text note to append, e.g. 'customer asked for a callback'. | |
| api_key | No | Optional credential for the order system. Its value is masked in the audit log. | |
| order_id | Yes | The order's ID, e.g. '4417'. Use list_orders to find one. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the annotations (readOnlyHint=false, destructiveHint=false), it discloses non-obvious traits: existing notes are preserved and nothing is overwritten or cancelled, no approval workflow is triggered, and every call is audit-logged. These are exactly the behaviors an agent cannot infer from the annotation flags alone.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, front-loaded with the core action, then the safety guarantee, then the alternative. Every sentence carries distinct information with no repetition of the name or schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, return values need no explanation, and annotations plus the schema cover the structured contract. The description supplies the remaining behavioral context (non-destructive append, no approval, audited) so nothing an agent needs to invoke this correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% — note, order_id and api_key are all documented in the schema, including the masking behavior of api_key and a pointer to list_orders for order_id. The description adds no parameter detail beyond that, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb+resource ('Append a note to an order') and immediately scopes the operation against the sibling 'cancel_order'. An agent can distinguish it from cancel_order/lookup_order without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly routes the agent away from this tool when the goal is to stop an order ('To actually stop an order, use cancel_order'), and notes the operation needs no approval. It does not state when this tool is the right choice versus merely adding a note through another path, but the boundary against the closest sibling is clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
cancel_orderADestructive
Cancel an order. Irreversible, so it is gated: a human must approve on the server's terminal before it runs. If they refuse, don't answer within two minutes, or there is no terminal to ask on, the call is denied and the refusal is logged. To record information without cancelling, use add_note.
| Name | Required | Description | Default |
|---|---|---|---|
| order_id | Yes | The order's ID, e.g. '4417'. Use list_orders to find one. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only declare destructiveHint=true and readOnlyHint=false; the description goes well beyond by disclosing irreversibility, the human-approval gate, the two-minute timeout, the no-terminal case, and that refusals are logged. This is exactly the operational context an agent needs before invoking a destructive tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the action and its irreversibility, then the gating mechanics, then the sibling alternative. No sentence is wasted and the ordering matches decision priority.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Output schema exists, so return values need not be described. For a one-parameter destructive tool, the description covers safety, gating, failure modes, and sibling routing – nothing material is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, and the single order_id parameter is already fully documented including an example and a pointer to list_orders. The description adds no parameter-level detail, so the baseline of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Cancel an order') in the first two words, and explicitly routes non-cancellation intent to the sibling add_note. An agent can distinguish it from list_orders, lookup_order, and add_note without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit when-to-use routing ('To record information without cancelling, use add_note') and detailed when-it-will-fail conditions (refusal, two-minute timeout, no terminal). The alternatives and the conditions that select them are named directly.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_ordersARead-only
List every order on the desk as id · customer · status, one per
line. Read-only. Start here to find the order ID that lookup_order,
add_note and cancel_order need.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and destructiveHint=false, so the 'Read-only' line merely confirms that safety profile. However, the description adds real value by disclosing the exact rendered output format, which is behavioral information beyond the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, zero waste. The purpose and output format are front-loaded, followed by the safety hint and the routing guidance.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Output schema exists so return values need not be explained, yet the description still notes the rendering format. For a zero-param read tool, nothing an agent needs in order to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool takes zero parameters, so the baseline is 4. There are no parameter semantics to clarify, and the description correctly says nothing misleading about inputs.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (List) and resource (orders), plus the exact output shape `id · customer · status`, one per line. Clearly distinguishable from the sibling operations that consume the IDs it produces.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
'Start here to find the order ID that lookup_order, add_note and cancel_order need' explicitly positions this as the entry point and names the alternatives that depend on its output. No inference required.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lookup_orderARead-only
Look up one order's customer, status and notes. Read-only; changes
nothing. Returns no order <id> if the ID doesn't exist.
| Name | Required | Description | Default |
|---|---|---|---|
| order_id | Yes | The order's ID, e.g. '4417'. Use list_orders to find one. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and destructiveHint=false, so 'Read-only; changes nothing' is largely redundant. What genuinely adds value is the disclosed failure behavior: it returns `no order <id>` for a missing ID, which tells the agent how to interpret an empty result.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences, no filler, with the primary purpose front-loaded and the error behavior follow-up. Every clause carries information the agent needs.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return-field explanation is unnecessary; the description still covers the one return nuance an agent must know (the 'no order <id>' sentinel). Nothing needed to invoke this correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and the single order_id parameter is documented with an example ('4417') and a pointer to list_orders. The description adds no parameter semantics beyond that, so the baseline of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (look up) and resource (one order) and enumerates the returned fields (customer, status, notes). The word 'one' cleanly separates it from list_orders, though the description never names that sibling explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the read-only framing and the singular 'one order', and the schema description points to list_orders for ID discovery. But the description itself gives no explicit when-to-use vs alternatives and no exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
4 tool updates
v0.3.0- Changed
add_note3 fields changed- added
Input schema / properties / api_key / descriptionAdded value: +"Optional credential for the order system. Its value is masked in the audit log." - added
Input schema / properties / note / descriptionAdded value: +"Free-text note to append, e.g. 'customer asked for a callback'." - added
Input schema / properties / order_id / descriptionAdded value: +"The order's ID, e.g. '4417'. Use list_orders to find one."
- Changed
cancel_order1 field changed- added
Input schema / properties / order_id / descriptionAdded value: +"The order's ID, e.g. '4417'. Use list_orders to find one."
- Added
list_orders - Changed
lookup_order1 field changed- added
Input schema / properties / order_id / descriptionAdded value: +"The order's ID, e.g. '4417'. Use list_orders to find one."
3 tool updates
v1.0.0- First observed
add_note - First observed
cancel_order - First observed
lookup_order
TDQS
Scored across 4 tools
Each tool has a clearly distinct action and target: list_orders lists all orders, lookup_order retrieves one, add_note appends only, and cancel_order performs the irreversible cancellation. The descriptions explicitly point agents to the correct alternative when tools might be confused.
All tools use a consistent snake_case verb_noun pattern: list_orders, lookup_order, add_note, cancel_order. The convention is predictable and easy to map to intent.
Four tools are well within the appropriate 3-15 range and each covers a distinct operation on the order desk. The surface is minimal but not under-scoped for the apparent purpose.
The set covers listing, lookup, note appending, and the irreversible cancellation path with approval gating. There are minor gaps such as creating new orders or updating order status/details, but core read/append/terminate workflows are covered.
Maintenance
Related MCP Connectors
Create, deploy, and operate MCP servers directly from your GitHub repositories.
A MCP server built for developers enabling Git based project management with project and personal…
MCP server for mandates, delegation, policy-gated execution, credential grants, and audit.
Independent A-F trust grade for any MCP server, watched for drift. Free, never for sale.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA least-privilege enforcement proxy for MCP servers. It sits between MCP clients and upstream servers, enforcing tool policies, hiding denied tools, requiring human approval for risky actions, and providing a structured audit trail.MIT
- AlicenseBqualityBmaintenanceLocal MCP server for inspecting and managing an allowlisted Discord server via Discord's REST API, with safety modes, idempotent JSON blueprints, and destructive-operation safeguards.271MIT
- AlicenseAqualityBmaintenanceA security-first MCP server for managing Whatbox slots with structured read-only inspection and approval-gated mutations, including storage, services, website deployment, and torrent control.30MIT
- AlicenseNot gradedqualityBmaintenanceEnforces identity-based access control and audit logging for MCP servers, letting you grant fine-grained tool permissions to users and systems while failing secure by default.MIT