mcp-gatehouse 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - run: pip install -e ".[dev]"
20
+ - run: pytest -q
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ audit.jsonl
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nick George
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-gatehouse
3
+ Version: 0.1.0
4
+ Summary: Permission tiers, approval gates, and audit logging for MCP servers — the server is the gatekeeper.
5
+ Project-URL: Homepage, https://nickgeorgeai.com/mcp
6
+ Project-URL: Repository, https://github.com/nickgeorgeseo/mcp-gatehouse
7
+ Author-email: Nick George <nick@nickgeorgeai.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai-agents,audit-log,mcp,model-context-protocol,permissions,security
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: mcp<2,>=1.27
22
+ Provides-Extra: dev
23
+ Requires-Dist: anyio>=4; extra == 'dev'
24
+ Requires-Dist: pytest>=8; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # mcp-gatehouse
28
+
29
+ <!-- mcp-name: io.github.nickgeorgeseo/gatehouse -->
30
+
31
+ [![CI](https://github.com/nickgeorgeseo/mcp-gatehouse/actions/workflows/ci.yml/badge.svg)](https://github.com/nickgeorgeseo/mcp-gatehouse/actions/workflows/ci.yml)
32
+ [![PyPI](https://img.shields.io/pypi/v/mcp-gatehouse)](https://pypi.org/project/mcp-gatehouse/)
33
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-gatehouse)](https://pypi.org/project/mcp-gatehouse/)
34
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
35
+
36
+ **Permission tiers, approval gates, and audit logging for MCP servers.**
37
+ The server is the gatekeeper: you decide what an AI can read, what it can
38
+ write, and what's off-limits — and every action gets logged.
39
+
40
+ Most MCP servers hand the model every tool at full strength and keep no
41
+ record of what it did. That's fine for a demo. It's not fine the day an
42
+ agent has write access to your CRM, your books, or your order system.
43
+ `mcp-gatehouse` is the missing gate, enforced **inside** the server — no
44
+ proxy, no external policy service, no dependencies beyond the official
45
+ [`mcp` SDK](https://github.com/modelcontextprotocol/python-sdk).
46
+
47
+ ```
48
+ pip install mcp-gatehouse
49
+ ```
50
+
51
+ ## What you get
52
+
53
+ | | |
54
+ |---|---|
55
+ | **Permission tiers** | Every tool is declared `READ`, `WRITE`, or `DESTRUCTIVE` — and the tier also emits honest spec `ToolAnnotations` (`readOnlyHint` / `destructiveHint`), which the wrapper won't let you override to lie. |
56
+ | **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. |
57
+ | **Audit log** | Append-only JSONL, one line per call — allowed, denied, or failed — with UTC timestamps and durations. The answer to "what did the AI actually do?" six months later. |
58
+ | **Redaction** | Argument keys you name (`api_key`, `password`, `token`, … by default) are masked before they reach the log *or* the approver. |
59
+ | **Denylist** | Block a tool outright, whatever its tier. |
60
+
61
+ ## Quickstart
62
+
63
+ ```python
64
+ from mcp.server.fastmcp import FastMCP
65
+ from mcp_gatehouse import AccessTier, AuditLog, Gatehouse, Policy
66
+
67
+ mcp = FastMCP("order-desk")
68
+ gatehouse = Gatehouse(
69
+ mcp,
70
+ policy=Policy(approver=lambda req: input(f"allow {req.tool}? [y/N] ") == "y"),
71
+ audit=AuditLog(path="audit.jsonl"),
72
+ )
73
+
74
+ @gatehouse.tool(tier=AccessTier.READ)
75
+ def lookup_order(order_id: str) -> str:
76
+ """Look up an order's status."""
77
+ ...
78
+
79
+ @gatehouse.tool(tier=AccessTier.DESTRUCTIVE)
80
+ def cancel_order(order_id: str) -> str:
81
+ """Cancel an order. Runs only if the approver says yes."""
82
+ ...
83
+
84
+ mcp.run()
85
+ ```
86
+
87
+ That's the whole integration: build your `FastMCP` server exactly as the
88
+ SDK docs show, but register tools through the gatehouse. Schema generation,
89
+ transports, and everything else work unchanged — the guard preserves the
90
+ function's signature.
91
+
92
+ Under the default policy, `DESTRUCTIVE` requires approval and everything
93
+ is audited. Gate writes too with one line:
94
+
95
+ ```python
96
+ Policy(require_approval=frozenset({AccessTier.WRITE, AccessTier.DESTRUCTIVE}), ...)
97
+ ```
98
+
99
+ What the audit trail looks like:
100
+
101
+ ```json
102
+ {"ts": "2026-07-16T14:02:11+00:00", "tool": "lookup_order", "tier": "read", "outcome": "ok", "arguments": {"order_id": "4417"}, "duration_ms": 0.42}
103
+ {"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}
104
+ {"ts": "2026-07-16T14:03:05+00:00", "tool": "cancel_order", "tier": "destructive", "outcome": "denied", "reason": "approver refused", "arguments": {"order_id": "4417"}}
105
+ ```
106
+
107
+ ## Try the demo
108
+
109
+ The package ships a runnable order-desk server with all three tiers wired
110
+ up and a terminal-prompt approver:
111
+
112
+ ```
113
+ mcp-gatehouse-demo
114
+ ```
115
+
116
+ Point any MCP client at it over stdio (Claude Desktop, etc.), ask the model
117
+ to cancel an order, and watch the approval land in your terminal — and the
118
+ verdict land in `audit.jsonl` either way. `examples/orders_server.py` is
119
+ the same server as a copyable template.
120
+
121
+ ## Design notes
122
+
123
+ - **Enforcement lives inside the server**, at the tool boundary. A proxy
124
+ can't see your tools' semantics, and a policy service is one more thing
125
+ to deploy. A 40-person plant doesn't have a platform team; this is a few
126
+ small classes and a JSONL file.
127
+ - **Fail closed.** Security defaults that quietly allow are worse than none.
128
+ That includes redaction: argument values the scrubber can't take apart
129
+ (arbitrary objects, bytes) are replaced with an opaque placeholder rather
130
+ than passed through, and exception *messages* stay out of the log —
131
+ only the exception type is recorded, because error text loves to embed
132
+ the very values you just redacted.
133
+ - **The audit log records denials and errors**, not just successes — the
134
+ calls that *didn't* happen are half the story.
135
+ - **A blocking terminal approver and the stdio transport don't mix** —
136
+ stdout/stdin are the protocol pipe. The demo's approver prompts on
137
+ `/dev/tty` for exactly that reason (and denies when no terminal exists).
138
+ Real deployments should approve out-of-band: Slack, a ticket, a queue.
139
+ - **What this is not:** authentication, transport encryption, or a sandbox.
140
+ It's a gate inside your server, not a perimeter around it. See
141
+ [SECURITY.md](SECURITY.md).
142
+
143
+ ## Compatibility
144
+
145
+ Targets the official [`mcp` Python SDK](https://github.com/modelcontextprotocol/python-sdk)
146
+ v1.x (`mcp>=1.27,<2`) and Python 3.10+. When SDK v2 ships for the
147
+ 2026-07-28 spec revision, a v2-compatible release will follow — the
148
+ public API here (`Gatehouse`, `Policy`, `AuditLog`, `AccessTier`) will
149
+ not change.
150
+
151
+ ## Who built this
152
+
153
+ [Nick George](https://nickgeorgeai.com) — I design and run MCP servers in
154
+ production for a mid-market reverse logistics-tech company, and build them
155
+ for businesses at [nickgeorgeai.com](https://nickgeorgeai.com). This
156
+ library is the permission-and-audit discipline from those builds, extracted.
157
+
158
+ If you're an owner or operator wondering what MCP even is, start with the
159
+ plain-English guide: [What is an MCP server?](https://nickgeorgeai.com/mcp)
160
+
161
+ ## License
162
+
163
+ [MIT](LICENSE)
@@ -0,0 +1,137 @@
1
+ # mcp-gatehouse
2
+
3
+ <!-- mcp-name: io.github.nickgeorgeseo/gatehouse -->
4
+
5
+ [![CI](https://github.com/nickgeorgeseo/mcp-gatehouse/actions/workflows/ci.yml/badge.svg)](https://github.com/nickgeorgeseo/mcp-gatehouse/actions/workflows/ci.yml)
6
+ [![PyPI](https://img.shields.io/pypi/v/mcp-gatehouse)](https://pypi.org/project/mcp-gatehouse/)
7
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-gatehouse)](https://pypi.org/project/mcp-gatehouse/)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
+
10
+ **Permission tiers, approval gates, and audit logging for MCP servers.**
11
+ The server is the gatekeeper: you decide what an AI can read, what it can
12
+ write, and what's off-limits — and every action gets logged.
13
+
14
+ Most MCP servers hand the model every tool at full strength and keep no
15
+ record of what it did. That's fine for a demo. It's not fine the day an
16
+ agent has write access to your CRM, your books, or your order system.
17
+ `mcp-gatehouse` is the missing gate, enforced **inside** the server — no
18
+ proxy, no external policy service, no dependencies beyond the official
19
+ [`mcp` SDK](https://github.com/modelcontextprotocol/python-sdk).
20
+
21
+ ```
22
+ pip install mcp-gatehouse
23
+ ```
24
+
25
+ ## What you get
26
+
27
+ | | |
28
+ |---|---|
29
+ | **Permission tiers** | Every tool is declared `READ`, `WRITE`, or `DESTRUCTIVE` — and the tier also emits honest spec `ToolAnnotations` (`readOnlyHint` / `destructiveHint`), which the wrapper won't let you override to lie. |
30
+ | **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. |
31
+ | **Audit log** | Append-only JSONL, one line per call — allowed, denied, or failed — with UTC timestamps and durations. The answer to "what did the AI actually do?" six months later. |
32
+ | **Redaction** | Argument keys you name (`api_key`, `password`, `token`, … by default) are masked before they reach the log *or* the approver. |
33
+ | **Denylist** | Block a tool outright, whatever its tier. |
34
+
35
+ ## Quickstart
36
+
37
+ ```python
38
+ from mcp.server.fastmcp import FastMCP
39
+ from mcp_gatehouse import AccessTier, AuditLog, Gatehouse, Policy
40
+
41
+ mcp = FastMCP("order-desk")
42
+ gatehouse = Gatehouse(
43
+ mcp,
44
+ policy=Policy(approver=lambda req: input(f"allow {req.tool}? [y/N] ") == "y"),
45
+ audit=AuditLog(path="audit.jsonl"),
46
+ )
47
+
48
+ @gatehouse.tool(tier=AccessTier.READ)
49
+ def lookup_order(order_id: str) -> str:
50
+ """Look up an order's status."""
51
+ ...
52
+
53
+ @gatehouse.tool(tier=AccessTier.DESTRUCTIVE)
54
+ def cancel_order(order_id: str) -> str:
55
+ """Cancel an order. Runs only if the approver says yes."""
56
+ ...
57
+
58
+ mcp.run()
59
+ ```
60
+
61
+ That's the whole integration: build your `FastMCP` server exactly as the
62
+ SDK docs show, but register tools through the gatehouse. Schema generation,
63
+ transports, and everything else work unchanged — the guard preserves the
64
+ function's signature.
65
+
66
+ Under the default policy, `DESTRUCTIVE` requires approval and everything
67
+ is audited. Gate writes too with one line:
68
+
69
+ ```python
70
+ Policy(require_approval=frozenset({AccessTier.WRITE, AccessTier.DESTRUCTIVE}), ...)
71
+ ```
72
+
73
+ What the audit trail looks like:
74
+
75
+ ```json
76
+ {"ts": "2026-07-16T14:02:11+00:00", "tool": "lookup_order", "tier": "read", "outcome": "ok", "arguments": {"order_id": "4417"}, "duration_ms": 0.42}
77
+ {"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}
78
+ {"ts": "2026-07-16T14:03:05+00:00", "tool": "cancel_order", "tier": "destructive", "outcome": "denied", "reason": "approver refused", "arguments": {"order_id": "4417"}}
79
+ ```
80
+
81
+ ## Try the demo
82
+
83
+ The package ships a runnable order-desk server with all three tiers wired
84
+ up and a terminal-prompt approver:
85
+
86
+ ```
87
+ mcp-gatehouse-demo
88
+ ```
89
+
90
+ Point any MCP client at it over stdio (Claude Desktop, etc.), ask the model
91
+ to cancel an order, and watch the approval land in your terminal — and the
92
+ verdict land in `audit.jsonl` either way. `examples/orders_server.py` is
93
+ the same server as a copyable template.
94
+
95
+ ## Design notes
96
+
97
+ - **Enforcement lives inside the server**, at the tool boundary. A proxy
98
+ can't see your tools' semantics, and a policy service is one more thing
99
+ to deploy. A 40-person plant doesn't have a platform team; this is a few
100
+ small classes and a JSONL file.
101
+ - **Fail closed.** Security defaults that quietly allow are worse than none.
102
+ That includes redaction: argument values the scrubber can't take apart
103
+ (arbitrary objects, bytes) are replaced with an opaque placeholder rather
104
+ than passed through, and exception *messages* stay out of the log —
105
+ only the exception type is recorded, because error text loves to embed
106
+ the very values you just redacted.
107
+ - **The audit log records denials and errors**, not just successes — the
108
+ calls that *didn't* happen are half the story.
109
+ - **A blocking terminal approver and the stdio transport don't mix** —
110
+ stdout/stdin are the protocol pipe. The demo's approver prompts on
111
+ `/dev/tty` for exactly that reason (and denies when no terminal exists).
112
+ Real deployments should approve out-of-band: Slack, a ticket, a queue.
113
+ - **What this is not:** authentication, transport encryption, or a sandbox.
114
+ It's a gate inside your server, not a perimeter around it. See
115
+ [SECURITY.md](SECURITY.md).
116
+
117
+ ## Compatibility
118
+
119
+ Targets the official [`mcp` Python SDK](https://github.com/modelcontextprotocol/python-sdk)
120
+ v1.x (`mcp>=1.27,<2`) and Python 3.10+. When SDK v2 ships for the
121
+ 2026-07-28 spec revision, a v2-compatible release will follow — the
122
+ public API here (`Gatehouse`, `Policy`, `AuditLog`, `AccessTier`) will
123
+ not change.
124
+
125
+ ## Who built this
126
+
127
+ [Nick George](https://nickgeorgeai.com) — I design and run MCP servers in
128
+ production for a mid-market reverse logistics-tech company, and build them
129
+ for businesses at [nickgeorgeai.com](https://nickgeorgeai.com). This
130
+ library is the permission-and-audit discipline from those builds, extracted.
131
+
132
+ If you're an owner or operator wondering what MCP even is, start with the
133
+ plain-English guide: [What is an MCP server?](https://nickgeorgeai.com/mcp)
134
+
135
+ ## License
136
+
137
+ [MIT](LICENSE)
@@ -0,0 +1,20 @@
1
+ # Security
2
+
3
+ `mcp-gatehouse` is security *tooling*, so it gets held to its own standard.
4
+
5
+ ## Reporting a vulnerability
6
+
7
+ Email **nick@nickgeorgeai.com** with the details. You'll get a reply
8
+ within 72 hours. Please don't open a public issue for anything exploitable —
9
+ give me a chance to ship the fix first.
10
+
11
+ ## What this library does and doesn't do
12
+
13
+ **Does:** enforce per-tool permission tiers, gate calls behind an approver
14
+ you control, keep an append-only audit trail, and mask configured argument
15
+ keys before they reach logs or approvers.
16
+
17
+ **Doesn't:** authenticate clients, encrypt transports, sandbox tool code,
18
+ or protect against a malicious *server author*. It's a gate inside your
19
+ server, not a perimeter around it. Treat third-party MCP servers with the
20
+ same scrutiny you'd give any software you install.
@@ -0,0 +1,59 @@
1
+ """A copyable template: the demo order-desk server, spelled out.
2
+
3
+ This is the same server `mcp-gatehouse-demo` runs — copy it and swap the
4
+ ORDERS dict for your real system. The approver comes from the package
5
+ because prompting a human is trickier than it looks: over the stdio
6
+ transport, stdout/stdin are the protocol pipe, so the prompt must go to
7
+ the controlling terminal (and fail closed when there isn't one).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from mcp.server.fastmcp import FastMCP
13
+
14
+ from mcp_gatehouse import AccessTier, AuditLog, Gatehouse, Policy
15
+ from mcp_gatehouse.demo import terminal_approver
16
+
17
+ # A stand-in for your real system of record.
18
+ ORDERS = {
19
+ "4417": {"customer": "Blue Ridge Cabinets", "status": "packed", "notes": []},
20
+ "4418": {"customer": "Piedmont Supply", "status": "picking", "notes": []},
21
+ }
22
+
23
+ mcp = FastMCP("order-desk")
24
+ gatehouse = Gatehouse(
25
+ mcp,
26
+ policy=Policy(approver=terminal_approver),
27
+ audit=AuditLog(path="audit.jsonl"),
28
+ )
29
+
30
+
31
+ @gatehouse.tool(tier=AccessTier.READ)
32
+ def lookup_order(order_id: str) -> str:
33
+ """Look up an order's status."""
34
+ order = ORDERS.get(order_id)
35
+ if order is None:
36
+ return f"no order {order_id}"
37
+ return f"{order_id} · {order['customer']} · {order['status']}"
38
+
39
+
40
+ @gatehouse.tool(tier=AccessTier.WRITE)
41
+ def add_note(order_id: str, note: str, api_key: str = "") -> str:
42
+ """Attach a note to an order. (The api_key never reaches the log.)"""
43
+ if order_id not in ORDERS:
44
+ return f"no order {order_id}"
45
+ ORDERS[order_id]["notes"].append(note)
46
+ return f"note added to {order_id}"
47
+
48
+
49
+ @gatehouse.tool(tier=AccessTier.DESTRUCTIVE)
50
+ def cancel_order(order_id: str) -> str:
51
+ """Cancel an order. Requires approval — the gate fails closed."""
52
+ if order_id not in ORDERS:
53
+ return f"no order {order_id}"
54
+ ORDERS[order_id]["status"] = "cancelled"
55
+ return f"{order_id} cancelled"
56
+
57
+
58
+ if __name__ == "__main__":
59
+ mcp.run()
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcp-gatehouse"
7
+ version = "0.1.0"
8
+ description = "Permission tiers, approval gates, and audit logging for MCP servers — the server is the gatekeeper."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Nick George", email = "nick@nickgeorgeai.com" }]
13
+ keywords = [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "ai-agents",
17
+ "security",
18
+ "audit-log",
19
+ "permissions",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Intended Audience :: Developers",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Topic :: Software Development :: Libraries",
30
+ "Topic :: Security",
31
+ ]
32
+ dependencies = ["mcp>=1.27,<2"]
33
+
34
+ [project.scripts]
35
+ mcp-gatehouse-demo = "mcp_gatehouse.demo:main"
36
+ # Alias so `uvx mcp-gatehouse` / `pipx run mcp-gatehouse` resolve without --from.
37
+ mcp-gatehouse = "mcp_gatehouse.demo:main"
38
+
39
+ [project.urls]
40
+ Homepage = "https://nickgeorgeai.com/mcp"
41
+ Repository = "https://github.com/nickgeorgeseo/mcp-gatehouse"
42
+
43
+ [project.optional-dependencies]
44
+ dev = ["pytest>=8", "anyio>=4"]
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/mcp_gatehouse"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.nickgeorgeseo/gatehouse",
4
+ "title": "mcp-gatehouse",
5
+ "description": "Permission tiers, approval gates, and audit logging for MCP servers, enforced inside the server at the tool boundary. Ships a runnable order-desk demo: READ tools run freely, WRITE tools are audited with secret redaction, DESTRUCTIVE tools require human approval and fail closed.",
6
+ "version": "0.1.0",
7
+ "websiteUrl": "https://nickgeorgeai.com/mcp",
8
+ "repository": {
9
+ "url": "https://github.com/nickgeorgeseo/mcp-gatehouse",
10
+ "source": "github"
11
+ },
12
+ "packages": [
13
+ {
14
+ "registryType": "pypi",
15
+ "registryBaseUrl": "https://pypi.org",
16
+ "identifier": "mcp-gatehouse",
17
+ "version": "0.1.0",
18
+ "runtimeHint": "uvx",
19
+ "transport": {
20
+ "type": "stdio"
21
+ }
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,30 @@
1
+ """mcp-gatehouse — permission tiers, approval gates, and audit logging
2
+ for MCP servers built with the official Python SDK.
3
+
4
+ The server is the gatekeeper: you decide what an AI can read, what it can
5
+ write, and what's off-limits — and every action gets logged.
6
+ """
7
+
8
+ from .audit import AuditLog, NullAuditLog
9
+ from .gatehouse import GateDenied, Gatehouse
10
+ from .policy import (
11
+ AccessTier,
12
+ ApprovalRequest,
13
+ Approver,
14
+ Policy,
15
+ redact_arguments,
16
+ )
17
+
18
+ __all__ = [
19
+ "AccessTier",
20
+ "ApprovalRequest",
21
+ "Approver",
22
+ "AuditLog",
23
+ "GateDenied",
24
+ "Gatehouse",
25
+ "NullAuditLog",
26
+ "Policy",
27
+ "redact_arguments",
28
+ ]
29
+
30
+ __version__ = "0.1.0"
@@ -0,0 +1,76 @@
1
+ """Append-only JSONL audit log.
2
+
3
+ One line per tool call — allowed, denied, or failed — with UTC timestamps
4
+ and redacted arguments. The point is a trail you can grep six months later
5
+ when someone asks "what did the AI actually do?"
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import datetime as _dt
11
+ import io
12
+ import json
13
+ import os
14
+ import threading
15
+ from pathlib import Path
16
+ from typing import Any, TextIO
17
+
18
+
19
+ class AuditLog:
20
+ """Writes one JSON object per line to a file or stream.
21
+
22
+ Thread-safe for concurrent tool calls within one process. Events are
23
+ flushed on every write — an audit log that loses its tail in a crash
24
+ is not an audit log.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ path: str | os.PathLike[str] | None = None,
30
+ stream: TextIO | None = None,
31
+ ) -> None:
32
+ if (path is None) == (stream is None):
33
+ raise ValueError("provide exactly one of `path` or `stream`")
34
+ self._lock = threading.Lock()
35
+ if path is not None:
36
+ p = Path(path)
37
+ p.parent.mkdir(parents=True, exist_ok=True)
38
+ self._stream: TextIO = p.open("a", encoding="utf-8")
39
+ self._owns_stream = True
40
+ else:
41
+ assert stream is not None
42
+ self._stream = stream
43
+ self._owns_stream = False
44
+
45
+ def record(self, **event: Any) -> None:
46
+ """Append one event. A ``ts`` field (UTC, ISO-8601) is added.
47
+
48
+ ``ensure_ascii=True`` on purpose: it escapes U+2028/U+2029/NEL and
49
+ friends, so a crafted argument value can never split one record
50
+ into two for line-aware readers. One ``\\n`` per record, always.
51
+ """
52
+ event = {"ts": _dt.datetime.now(_dt.timezone.utc).isoformat(), **event}
53
+ line = json.dumps(event, ensure_ascii=True, default=str)
54
+ with self._lock:
55
+ self._stream.write(line + "\n")
56
+ self._stream.flush()
57
+
58
+ def close(self) -> None:
59
+ if self._owns_stream:
60
+ self._stream.close()
61
+
62
+ def __enter__(self) -> "AuditLog":
63
+ return self
64
+
65
+ def __exit__(self, *exc: object) -> None:
66
+ self.close()
67
+
68
+
69
+ class NullAuditLog(AuditLog):
70
+ """An audit log that swallows everything. For tests and dry runs."""
71
+
72
+ def __init__(self) -> None:
73
+ super().__init__(stream=io.StringIO())
74
+
75
+ def record(self, **event: Any) -> None: # noqa: D102 — intentionally inert
76
+ pass