gatehouse
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@gatehouseshow me today's audit log of blocked calls from the support-write profile"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
gatehouse
The guardrail layer you would otherwise rebuild inside every customer's MCP server.
An agent platform attaches tools per server. The customer's server exposes everything it has: the refund tool next to the lookup tool, the IBAN next to the name. So the rules that matter in a regulated deployment (bank changes go to a person, the model never reads a document number, a retried write never runs twice) end up hand-written into each customer's tool code, or into a prompt, where a model can talk its way past them.
gatehouse is an MCP proxy that sits between the agent and the customer's MCP server and enforces those rules in code, from one YAML file:
Profiles | Each gatehouse instance serves one profile. Tools outside it are not listed and cannot be called. Attaching the profile is the permission grant. |
Rules |
|
Masking | Every result is masked before the model sees it: named fields ( |
Idempotency | A retried write with the same arguments returns the first result instead of running again. |
Audit | One JSON line per call: profile, tool, masked arguments, decision, rule, latency. |
Shadow mode |
|
Auth | Checks the bearer key on every request ( |
The customer's tools do not change. The agent's prompt does not change.
Where it sits
flowchart LR
subgraph agent["Agent runtime (unchanged)"]
direction TB
P["Policies and routines<br/>decide what the agent should do"]
R["LLM router"]
M1["mcps: operator-read"]
M2["mcps: operator-write"]
end
subgraph gh["gatehouse, one process per profile"]
direction TB
A["1 Bearer check<br/>MCP_API_KEY on every request"]
F["2 Profile<br/>only granted tools are listed"]
X["3 Rules<br/>allow, deny or handoff"]
I["4 Idempotency<br/>a retried write returns the first result"]
K["5 Mask<br/>fields and PII in free text"]
L["6 Audit<br/>one JSON line per call"]
A --> F --> X --> I
K --> L
end
subgraph up["Customer's MCP server (unchanged)"]
direction TB
U1["Player accounts"]
U2["Support desk"]
U3["KYC provider"]
U4["Payments"]
end
M2 -- "tools/call" --> A
I -- "allowed call" --> up
up -- "raw result" --> K
L -- "masked result" --> M2
X -. "handoff: needs_human as data,<br/>upstream untouched" .-> M2A call the rules refuse never reaches the customer's systems. A call they allow comes back masked. Every call leaves one audit line.
Related MCP server: cordon
One call, decided
flowchart TD
C(["tools/call from the agent"]) --> K{"Bearer key valid?"}
K -- no --> E1["401, nothing runs"]
K -- yes --> P{"Tool in this profile?"}
P -- no --> D1["not_allowed, rule: profile<br/>(enforced even in shadow mode)"]
P -- yes --> R{"First matching rule"}
R -- "deny or handoff" --> S{"mode: shadow?"}
S -- no --> D2["return the error as data<br/>handoff names initiate_human_handoff"]
S -- yes --> Q
R -- "no match: allow" --> Q{"Same write seen<br/>inside the window?"}
R -- "rule cannot read an argument" --> D3["deny, fail closed"]
Q -- yes --> RP["return the first result, run nothing"]
Q -- no --> UP["call the upstream tool"]
UP --> MK["mask the result:<br/>named fields, PII in text,<br/>withhold images and files"]
MK --> OUT(["masked result to the agent"])
D1 & D2 & D3 & RP & OUT --> AU[("audit.jsonl<br/>masked arguments, decision, rule, ms")]A handoff, end to end
sequenceDiagram
autonumber
participant Player
participant Agent as Agent (policies, router)
participant Gate as gatehouse (support-write)
participant PAM as Player accounts
participant Human as Support person
Player->>Agent: "Please pay my winnings to a new IBAN"
Agent->>Gate: tools/call update_bank_account(P-1001, ES76...)
Gate->>Gate: rule bank-account-changes-go-to-a-human matches
Gate-->>Agent: {"error": "needs_human", "rule": "...", "next": "initiate_human_handoff"}
Note over Gate,PAM: The player account is never called
Agent->>Human: built-in:initiate_human_handoff(reason)
Human->>PAM: verifies identity, changes the IBAN
Agent->>Gate: tools/call get_player(P-1001)
Gate->>PAM: get_player(P-1001)
PAM-->>Gate: full record, IBAN, phone, date of birth
Gate-->>Agent: iban ****5766, phone ****5678, date of birth droppedTwo minutes, no keys
pip install -e .
python examples/igaming/demo.pyA support agent on a regulated iGaming desk (player accounts, tickets, KYC; all data fake):
support-read sees 3 of 9 upstream tools: get_player, get_ticket, list_transactions
support-write sees 8 of 9 upstream tools: add_ticket_comment, apply_bonus, close_ticket, get_player, ...
look up the player get_player -> iban ****1332, phone ****5678, email ***@example.com, dropped date_of_birth, document_number
read a ticket with an IBAN in it get_ticket -> "Withdrawal of 40 EUR pending 3 days. Please send it to ****1332, or call me on ****5678."
player asks to change the payout IBAN update_bank_account -> NEEDS_HUMAN [bank-account-changes-go-to-a-human]
10 EUR goodwill bonus apply_bonus -> {"bonus_id": "B-9001", ..., "new_balance_eur": 52.5}
model retries after a timeout apply_bonus -> {"bonus_id": "B-9001", ..., "new_balance_eur": 52.5}
120 EUR bonus apply_bonus -> NEEDS_HUMAN [bonus-above-50-needs-a-supervisor]
player asks to self-exclude set_self_exclusion -> NEEDS_HUMAN [self-exclusion-goes-to-a-human]
close with no resolution close_ticket -> NOT_ALLOWED [close-only-with-a-resolution]
a tool in no profile export_player_data -> NOT_ALLOWED [profile]
balance after two identical 10 EUR calls: 52.50 EUR (started at 42.50)The config
upstream: http://operator-mcp:8765/mcp # any streamable-http MCP server, or a local .py file
api_key: ${MCP_API_KEY}
audit_log: audit.jsonl
profiles:
support-read: { tools: [get_player, list_transactions, get_ticket] }
support-write: { tools: [get_player, list_transactions, get_ticket, add_ticket_comment,
close_ticket, apply_bonus, update_bank_account, set_self_exclusion] }
mask:
fields: { iban: last4, phone: last4, email: domain, date_of_birth: drop, document_number: drop }
patterns: [iban, card, email, phone]
rules:
- id: bank-account-changes-go-to-a-human
tools: [update_bank_account]
action: handoff
reason: Bank account changes are always handled by a person.
- id: bonus-above-50-needs-a-supervisor
tools: [apply_bonus]
when: { amount_eur: { gt: 50 } }
action: handoff
idempotency:
tools: [apply_bonus, add_ticket_comment, update_bank_account]
window_seconds: 600Conditions: gt, gte, lt, lte, equals, in, matches (regex), missing. The full example is
examples/igaming/gatehouse.yaml.
Running it in front of a real server
gatehouse examples/igaming/gatehouse.yaml --profile support-write --port 8766It serves streamable-http at /mcp. In an InteractiveAI agent manifest it is an ordinary MCP entry,
one per profile:
agent_config:
mcps:
- id: operator # tools appear as operator:get_player
hostname: http://gatehouse-support-write
port: 8766
api_key: ${GATEHOUSE_KEY}Or as a platform-hosted MCP, from the included Dockerfile:
iai mcps create gatehouse-support-write --image-name gatehouse --image-tag v1 --port 8766Tests
python -m unittest discover -s tests29 tests: every rule; shadow mode; masking edge cases (only Luhn-valid card numbers, amounts and ticket ids untouched); the idempotency window; the full proxy in memory; a real streamable-http run that refuses a call without the bearer key and still applies the rules with it; and a set of bypass regressions.
Fails closed
A guardrail that can be argued past is a suggestion. gatehouse reads arguments the way the upstream's validator will execute them, and refuses whatever it cannot read:
"true","1","yes"are true and"120"is 120, so a rule never decides on a string while the tool runs on the coerced value.NaN, infinities and booleans in an amount are refused, not compared (NaN > 50is false).A rule that cannot evaluate an argument denies the call; whitespace counts as missing.
IBAN,ibananddateOfBirth/date_of_birthare the same field to the masker.Content it cannot inspect (an image of an ID document, an embedded file) is withheld.
It proxies tools only. Upstream resources and prompts would bypass every rule and mask, so they are neither listed nor readable through gatehouse.
Shadow mode softens rules, never the profile: a tool outside the profile is refused in every mode.
The bearer key is compared in constant time, and if the config asks for a key that resolves empty, gatehouse refuses to start rather than serving unauthenticated.
Limits, honestly
Masking is pattern- and field-based. A secret paraphrased into prose with no recognisable shape gets through; the field rules are the first line, a human on irreversible actions the last.
Idempotency is an in-process dict: correct for one replica, Redis for several.
One process per profile. Per-tool permissions inside the platform's own manifest would make that unnecessary, and would be the better long-term home for this.
MIT. Built by Sri Krishna Vamsi D.
This server cannot be deployed
Maintenance
Related MCP Connectors
Security & DLP proxy for MCP: tool-poisoning scans, PII redaction on tool args/results. Beta.
Zero-secret MCP gateway for AI agents: risk-scored, audited calls with human-in-the-loop approval.
- gatewayOAuthai.sealgate
MCP gateway with runtime security policy, tool-call-level control, and audit of agent actions.
AgentGuard — 20-tool AI safety MCP: policy preflight, risk scoring, audit logging, rate limits.
Related MCP Servers
AlicenseNot gradedqualityAmaintenanceOpen-source MCP proxy that enforces security policies, content scanning, and audit logging between AI agents and tool servers25AGPL 3.0- AlicenseNot gradedqualityAmaintenanceSecurity gateway for MCP tool calls. Sits between your LLM client and MCP servers, enforcing per-tool policies (allow/block/approve/read-only), logging every call, and pausing dangerous operations for human approval in terminal or Slack.4 npm1MIT
- AlicenseAqualityCmaintenanceProvides a security governance layer for AI agents to safely access upstream MCP servers, enforcing tool-level RBAC, parameter constraints, authentication via static tokens or OIDC, and tamper-evident audit logging.21Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables developers to wrap existing MCP servers with a deterministic policy enforcement gate that intercepts tool discovery and tools/call requests over stdio, allowing only authorized calls to reach the downstream server. It supports dynamic tool exposure or hiding, allow/deny/approval-required decisions, configurable error disclosure modes, and Ed25519-signed retry credentials for approved workflows.Apache 2.0