arc-gate-mcp 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,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: arc-gate-mcp
3
+ Version: 0.1.0
4
+ Summary: Runtime governance for MCP tool calls — Arc Gate for the MCP protocol layer
5
+ Author-email: Hannah Nine <9hannahnine@gmail.com>
6
+ License: AGPL-3.0
7
+ Project-URL: Homepage, https://bendexgeometry.com/gate
8
+ Project-URL: Repository, https://github.com/9hannahnine-jpg/arc-gate-mcp
9
+ Keywords: mcp,prompt-injection,ai-security,llm,agent,runtime-governance
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: mcp>=1.0.0
13
+ Requires-Dist: httpx>=0.25.0
14
+
15
+ # arc-gate-mcp
16
+
17
+ **Runtime governance for MCP tool calls.**
18
+
19
+ Arc Gate MCP sits between your agent and any MCP server. It intercepts all tool call results and enforces instruction-authority boundaries before the agent processes them.
20
+
21
+ When a tool result contains injected instructions — a poisoned document, a malicious webpage, a hostile database row — Arc Gate blocks them before they reach the agent.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install arc-gate-mcp
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### Full proxy (wraps any MCP server)
32
+
33
+ ```python
34
+ from arc_gate_mcp import ArcGateMCPProxy
35
+
36
+ proxy = ArcGateMCPProxy(
37
+ upstream_url="http://localhost:8000/sse",
38
+ policy_mode="rag_assistant",
39
+ )
40
+ proxy.run()
41
+ ```
42
+
43
+ ### Per-tool guard
44
+
45
+ ```python
46
+ from arc_gate_mcp import ArcGateToolGuard
47
+
48
+ guard = ArcGateToolGuard(policy_mode="rag_assistant")
49
+
50
+ @mcp.tool()
51
+ async def read_document(path: str) -> str:
52
+ content = read_file(path)
53
+ return guard.check(content, tool_name="read_document")
54
+ ```
55
+
56
+ ### CLI
57
+
58
+ ```bash
59
+ arc-gate-mcp --upstream http://localhost:8000/sse --policy rag_assistant
60
+ ```
61
+
62
+ ## Policy modes
63
+
64
+ | Mode | Behavior |
65
+ |---|---|
66
+ | `balanced` | Block on detected injection |
67
+ | `browser_agent` | Strip injections, allow safe content |
68
+ | `finance_agent` | Strictest — block everything suspicious |
69
+ | `rag_assistant` | Strip injections, preserve safe data |
70
+
71
+ ## Related
72
+
73
+ - [Arc Gate](https://github.com/9hannahnine-jpg/arc-gate) — OpenAI-compatible proxy version
74
+ - [arc-sentry](https://github.com/9hannahnine-jpg/arc-sentry) — Whitebox detector for self-hosted models
75
+
76
+ ## License
77
+
78
+ AGPL-3.0. Commercial license available — contact 9hannahnine@gmail.com.
@@ -0,0 +1,64 @@
1
+ # arc-gate-mcp
2
+
3
+ **Runtime governance for MCP tool calls.**
4
+
5
+ Arc Gate MCP sits between your agent and any MCP server. It intercepts all tool call results and enforces instruction-authority boundaries before the agent processes them.
6
+
7
+ When a tool result contains injected instructions — a poisoned document, a malicious webpage, a hostile database row — Arc Gate blocks them before they reach the agent.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install arc-gate-mcp
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Full proxy (wraps any MCP server)
18
+
19
+ ```python
20
+ from arc_gate_mcp import ArcGateMCPProxy
21
+
22
+ proxy = ArcGateMCPProxy(
23
+ upstream_url="http://localhost:8000/sse",
24
+ policy_mode="rag_assistant",
25
+ )
26
+ proxy.run()
27
+ ```
28
+
29
+ ### Per-tool guard
30
+
31
+ ```python
32
+ from arc_gate_mcp import ArcGateToolGuard
33
+
34
+ guard = ArcGateToolGuard(policy_mode="rag_assistant")
35
+
36
+ @mcp.tool()
37
+ async def read_document(path: str) -> str:
38
+ content = read_file(path)
39
+ return guard.check(content, tool_name="read_document")
40
+ ```
41
+
42
+ ### CLI
43
+
44
+ ```bash
45
+ arc-gate-mcp --upstream http://localhost:8000/sse --policy rag_assistant
46
+ ```
47
+
48
+ ## Policy modes
49
+
50
+ | Mode | Behavior |
51
+ |---|---|
52
+ | `balanced` | Block on detected injection |
53
+ | `browser_agent` | Strip injections, allow safe content |
54
+ | `finance_agent` | Strictest — block everything suspicious |
55
+ | `rag_assistant` | Strip injections, preserve safe data |
56
+
57
+ ## Related
58
+
59
+ - [Arc Gate](https://github.com/9hannahnine-jpg/arc-gate) — OpenAI-compatible proxy version
60
+ - [arc-sentry](https://github.com/9hannahnine-jpg/arc-sentry) — Whitebox detector for self-hosted models
61
+
62
+ ## License
63
+
64
+ AGPL-3.0. Commercial license available — contact 9hannahnine@gmail.com.
@@ -0,0 +1,11 @@
1
+ """
2
+ Arc Gate MCP — Runtime governance for MCP tool calls.
3
+ """
4
+ from .arc_gate_mcp import (
5
+ ArcGateMCPProxy,
6
+ ArcGateToolGuard,
7
+ GovernanceDecision,
8
+ )
9
+
10
+ __version__ = "0.1.0"
11
+ __all__ = ["ArcGateMCPProxy", "ArcGateToolGuard", "GovernanceDecision"]
@@ -0,0 +1,474 @@
1
+ """
2
+ arc-gate-mcp — Runtime governance for MCP tool calls
3
+ =====================================================
4
+ Arc Gate MCP sits between your MCP client (agent) and any MCP server.
5
+ It intercepts all tool call results and enforces instruction-authority
6
+ boundaries before the agent processes them.
7
+
8
+ When a tool result contains injected instructions, Arc Gate blocks them
9
+ before they reach the agent — the same guarantee as the OpenAI proxy,
10
+ but for the MCP protocol layer.
11
+
12
+ Usage:
13
+ # Wrap any MCP server
14
+ from arc_gate_mcp import ArcGateMCPProxy
15
+
16
+ proxy = ArcGateMCPProxy(
17
+ upstream_url="http://localhost:8000/sse", # your MCP server
18
+ policy_mode="rag_assistant", # or browser_agent, finance_agent
19
+ arc_gate_url="https://web-production-6e47f.up.railway.app/v1/chat/completions",
20
+ api_key="your-key",
21
+ )
22
+ proxy.run()
23
+
24
+ Architecture:
25
+ Agent → Arc Gate MCP Proxy → Upstream MCP Server
26
+
27
+ Tool result intercepted
28
+ Authority state machine checks source
29
+ If injection detected: result sanitized / blocked
30
+ If clean: result passed through unchanged
31
+ """
32
+
33
+ import asyncio
34
+ import json
35
+ import os
36
+ import re
37
+ import uuid
38
+ from dataclasses import dataclass
39
+ from typing import Any, Optional
40
+ from urllib.parse import urlparse
41
+
42
+ import httpx
43
+ from mcp.server.fastmcp import FastMCP
44
+ from mcp import ClientSession
45
+ from mcp.client.sse import sse_client
46
+ from mcp.types import (
47
+ CallToolResult,
48
+ TextContent,
49
+ Tool,
50
+ )
51
+
52
+ # ── Authority detection (mirrors arc_authority_state.py logic) ────────────────
53
+
54
+ TOOL_POISON_PATTERNS = [
55
+ r"<information>",
56
+ r"</information>",
57
+ r"this\s+is\s+an?\s+important\s+message\s+from\s+me",
58
+ r"before\s+you\s+can\s+solve\s+the\s+task",
59
+ r"please\s+do\s+the\s+following\s+first",
60
+ r"after\s+you\s+do\s+that.{0,30}you\s+can\s+solve",
61
+ r"you\s+should\s+use\s+the\s+tool\s+`?\w+`?",
62
+ r"signed,?\s*\n\s*\w+",
63
+ r"(assistant|agent|ai|llm|model|system|gpt|claude)\s*[,:]?\s*(ignore|disregard|forget|override|bypass|reveal|follow|obey|comply)",
64
+ r"(for\s+ai\s+(readers?|systems?|agents?)|note\s+to\s+(ai|assistant|agent|llm))",
65
+ r"(hidden|invisible|secret)\s+(instruction|command|directive|note)",
66
+ r"(this\s+(tool|document|webpage|email|result|output|database|file))\s+(supersedes?|overrides?|replaces?)",
67
+ r"follow\s+(the\s+)?(instructions?|directive|command)\s+(in|from|inside|within|embedded\s+in)\s+(this|the)",
68
+ r"ignore\s+(all\s+)?(previous|prior|earlier|above|your)\s+instructions?",
69
+ r"disregard\s+(all\s+)?(previous|prior|earlier|above|your)\s+instructions?",
70
+ r"override\s+(system|developer|operator|admin)\s+instructions?",
71
+ r"bypass\s+(safety|security|content|policy|all)\s+(filter|guard|check|rule|restriction)s?",
72
+ r"system\s+(override|command|directive|instruction)",
73
+ r"\[system\]",
74
+ r"### system",
75
+ r"your\s+new\s+instructions?\s+(are|is|follow)",
76
+ ]
77
+
78
+ _COMPILED_PATTERNS = [
79
+ re.compile(p, re.IGNORECASE | re.DOTALL)
80
+ for p in TOOL_POISON_PATTERNS
81
+ ]
82
+
83
+
84
+ def _detect_injection(text: str) -> tuple[bool, Optional[str]]:
85
+ """Check tool result text for injection attempts. Returns (detected, matched_pattern)."""
86
+ import unicodedata, base64, codecs
87
+
88
+ def _norm(s):
89
+ return unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
90
+
91
+ variants = [text, _norm(text)]
92
+
93
+ # Base64 decode attempt
94
+ for chunk in re.findall(r'[A-Za-z0-9+/]{20,}={0,2}', text):
95
+ try:
96
+ decoded = base64.b64decode(chunk).decode('utf-8', errors='ignore')
97
+ if len(decoded) > 10:
98
+ variants.append(decoded)
99
+ except Exception:
100
+ pass
101
+
102
+ # ROT13
103
+ try:
104
+ variants.append(codecs.decode(text, 'rot13'))
105
+ except Exception:
106
+ pass
107
+
108
+ for variant in variants:
109
+ for pattern in _COMPILED_PATTERNS:
110
+ m = pattern.search(variant)
111
+ if m:
112
+ return True, m.group(0)[:80]
113
+
114
+ return False, None
115
+
116
+
117
+ @dataclass
118
+ class GovernanceDecision:
119
+ blocked: bool
120
+ decision: str # allow | restricted_continue | block
121
+ reason: str
122
+ matched_pattern: Optional[str]
123
+ sanitized_content: Optional[str] = None
124
+
125
+
126
+ def _govern_tool_result(
127
+ tool_name: str,
128
+ result_text: str,
129
+ policy_mode: str = "balanced",
130
+ ) -> GovernanceDecision:
131
+ """
132
+ Apply Arc Gate governance to a tool result.
133
+ Returns a GovernanceDecision with the enforcement action.
134
+ """
135
+ detected, matched = _detect_injection(result_text)
136
+
137
+ if not detected:
138
+ return GovernanceDecision(
139
+ blocked=False,
140
+ decision="allow",
141
+ reason="no_violation",
142
+ matched_pattern=None,
143
+ )
144
+
145
+ # Finance agent: strictest — block everything
146
+ if policy_mode == "finance_agent":
147
+ return GovernanceDecision(
148
+ blocked=True,
149
+ decision="block",
150
+ reason="source_boundary_violation",
151
+ matched_pattern=matched,
152
+ )
153
+
154
+ # Browser agent: block tool calls but allow safe text continuation
155
+ if policy_mode == "browser_agent":
156
+ safe = _sanitize_result(result_text)
157
+ return GovernanceDecision(
158
+ blocked=False,
159
+ decision="restricted_continue",
160
+ reason="source_boundary_violation",
161
+ matched_pattern=matched,
162
+ sanitized_content=safe,
163
+ )
164
+
165
+ # RAG assistant: strip injection, return safe content
166
+ if policy_mode == "rag_assistant":
167
+ safe = _sanitize_result(result_text)
168
+ return GovernanceDecision(
169
+ blocked=False,
170
+ decision="restricted_continue",
171
+ reason="source_boundary_violation",
172
+ matched_pattern=matched,
173
+ sanitized_content=safe,
174
+ )
175
+
176
+ # Balanced default: block
177
+ return GovernanceDecision(
178
+ blocked=True,
179
+ decision="block",
180
+ reason="source_boundary_violation",
181
+ matched_pattern=matched,
182
+ )
183
+
184
+
185
+ def _sanitize_result(text: str) -> str:
186
+ """Strip injection content from tool result, preserving safe data."""
187
+ # Remove <INFORMATION>...</INFORMATION> blocks
188
+ text = re.sub(
189
+ r'<INFORMATION>.*?</INFORMATION>',
190
+ '[Arc Gate: Injected instruction removed]',
191
+ text, flags=re.IGNORECASE | re.DOTALL
192
+ )
193
+ # Remove lines that match injection patterns
194
+ lines = []
195
+ for line in text.split('\n'):
196
+ detected, _ = _detect_injection(line)
197
+ if not detected:
198
+ lines.append(line)
199
+ else:
200
+ lines.append('[Arc Gate: Instruction removed]')
201
+ return '\n'.join(lines)
202
+
203
+
204
+ # ── Arc Gate MCP Proxy ────────────────────────────────────────────────────────
205
+
206
+ class ArcGateMCPProxy:
207
+ """
208
+ Transparent MCP proxy that enforces instruction-authority boundaries
209
+ on all tool call results before they reach the agent.
210
+
211
+ Wraps any upstream MCP server and exposes the same tool interface.
212
+ Injected instructions in tool results are blocked or sanitized
213
+ depending on the policy mode.
214
+ """
215
+
216
+ def __init__(
217
+ self,
218
+ upstream_url: str,
219
+ policy_mode: str = "balanced",
220
+ arc_gate_url: Optional[str] = None,
221
+ api_key: Optional[str] = None,
222
+ server_name: str = "arc-gate-mcp",
223
+ ):
224
+ self.upstream_url = upstream_url
225
+ self.policy_mode = policy_mode
226
+ self.arc_gate_url = arc_gate_url or os.environ.get(
227
+ "ARC_GATE_URL",
228
+ "https://web-production-6e47f.up.railway.app/v1/chat/completions"
229
+ )
230
+ self.api_key = api_key or os.environ.get("OPENAI_API_KEY", "demo")
231
+ self.server_name = server_name
232
+ self.mcp = FastMCP(server_name)
233
+ self._upstream_tools: list[Tool] = []
234
+ self._session_id = f"mcp_proxy_{uuid.uuid4().hex[:12]}"
235
+ self._blocked_count = 0
236
+ self._allowed_count = 0
237
+
238
+ async def _fetch_upstream_tools(self) -> list[Tool]:
239
+ """Connect to upstream MCP server and discover available tools."""
240
+ async with sse_client(self.upstream_url) as (read, write):
241
+ async with ClientSession(read, write) as session:
242
+ await session.initialize()
243
+ result = await session.list_tools()
244
+ return result.tools
245
+
246
+ async def _call_upstream_tool(
247
+ self, tool_name: str, arguments: dict
248
+ ) -> CallToolResult:
249
+ """Call a tool on the upstream MCP server."""
250
+ async with sse_client(self.upstream_url) as (read, write):
251
+ async with ClientSession(read, write) as session:
252
+ await session.initialize()
253
+ return await session.call_tool(tool_name, arguments)
254
+
255
+ def _make_blocked_result(self, tool_name: str, matched: str) -> CallToolResult:
256
+ """Return a safe blocked result with Arc Gate metadata."""
257
+ return CallToolResult(
258
+ content=[TextContent(
259
+ type="text",
260
+ text=(
261
+ f"[Arc Gate] Tool result from '{tool_name}' was blocked.\n"
262
+ f"Reason: Untrusted content attempted instruction-authority transfer.\n"
263
+ f"Matched pattern: {matched}\n"
264
+ f"Session: {self._session_id}\n"
265
+ f"Policy: {self.policy_mode}\n\n"
266
+ f"The tool call completed but the result contained injected instructions "
267
+ f"that were prevented from reaching the agent."
268
+ )
269
+ )],
270
+ isError=False,
271
+ )
272
+
273
+ def _make_restricted_result(
274
+ self, tool_name: str, safe_content: str, matched: str
275
+ ) -> CallToolResult:
276
+ """Return sanitized result with Arc Gate warning."""
277
+ return CallToolResult(
278
+ content=[TextContent(
279
+ type="text",
280
+ text=(
281
+ f"[Arc Gate: RESTRICTED_CONTINUE] Injected instructions removed from "
282
+ f"'{tool_name}' result. Safe content preserved.\n\n"
283
+ f"{safe_content}"
284
+ )
285
+ )],
286
+ isError=False,
287
+ )
288
+
289
+ async def _governed_tool_call(
290
+ self, tool_name: str, arguments: dict
291
+ ) -> CallToolResult:
292
+ """Call upstream tool and apply governance to the result."""
293
+ # Call upstream
294
+ result = await self._call_upstream_tool(tool_name, arguments)
295
+
296
+ # Extract text content for inspection
297
+ full_text = "\n".join(
298
+ block.text for block in result.content
299
+ if hasattr(block, "text")
300
+ )
301
+
302
+ # Apply governance
303
+ decision = _govern_tool_result(full_text, tool_name, self.policy_mode)
304
+
305
+ if decision.blocked:
306
+ self._blocked_count += 1
307
+ print(
308
+ f"[Arc Gate] BLOCKED tool='{tool_name}' "
309
+ f"pattern='{decision.matched_pattern}' "
310
+ f"session={self._session_id[:16]}"
311
+ )
312
+ return self._make_blocked_result(tool_name, decision.matched_pattern or "")
313
+
314
+ if decision.decision == "restricted_continue":
315
+ self._blocked_count += 1
316
+ print(
317
+ f"[Arc Gate] RESTRICTED_CONTINUE tool='{tool_name}' "
318
+ f"pattern='{decision.matched_pattern}' "
319
+ f"session={self._session_id[:16]}"
320
+ )
321
+ return self._make_restricted_result(
322
+ tool_name,
323
+ decision.sanitized_content or full_text,
324
+ decision.matched_pattern or "",
325
+ )
326
+
327
+ self._allowed_count += 1
328
+ return result
329
+
330
+ async def _setup(self):
331
+ """Discover upstream tools and register governed versions."""
332
+ print(f"[Arc Gate MCP] Connecting to upstream: {self.upstream_url}")
333
+ tools = await self._fetch_upstream_tools()
334
+ self._upstream_tools = tools
335
+ print(f"[Arc Gate MCP] Discovered {len(tools)} tools: {[t.name for t in tools]}")
336
+
337
+ for tool in tools:
338
+ # Capture tool in closure
339
+ tool_name = tool.name
340
+
341
+ async def make_handler(name: str):
342
+ async def handler(**kwargs) -> str:
343
+ result = await self._governed_tool_call(name, kwargs)
344
+ texts = [
345
+ block.text for block in result.content
346
+ if hasattr(block, "text")
347
+ ]
348
+ return "\n".join(texts)
349
+ return handler
350
+
351
+ handler = await make_handler(tool_name)
352
+ handler.__doc__ = (
353
+ f"{tool.description or tool_name}\n\n"
354
+ f"[Protected by Arc Gate — policy: {self.policy_mode}]"
355
+ )
356
+
357
+ self.mcp.add_tool(
358
+ handler,
359
+ name=tool_name,
360
+ description=handler.__doc__,
361
+ )
362
+
363
+ print(f"[Arc Gate MCP] Proxy ready. Policy: {self.policy_mode}")
364
+ print(f"[Arc Gate MCP] Session: {self._session_id}")
365
+
366
+ def run(self, transport: str = "stdio"):
367
+ """Start the Arc Gate MCP proxy server."""
368
+ async def _run():
369
+ await self._setup()
370
+ if transport == "sse":
371
+ await self.mcp.run_sse_async()
372
+ else:
373
+ await self.mcp.run_stdio_async()
374
+
375
+ asyncio.run(_run())
376
+
377
+ def stats(self) -> dict:
378
+ return {
379
+ "session_id": self._session_id,
380
+ "policy_mode": self.policy_mode,
381
+ "upstream_url": self.upstream_url,
382
+ "tools_proxied": len(self._upstream_tools),
383
+ "blocked": self._blocked_count,
384
+ "allowed": self._allowed_count,
385
+ }
386
+
387
+
388
+ # ── Standalone governance checker (no upstream required) ─────────────────────
389
+
390
+ class ArcGateToolGuard:
391
+ """
392
+ Lightweight tool result governance for use without a full MCP proxy.
393
+ Drop into any existing MCP tool handler to protect individual tools.
394
+
395
+ Usage:
396
+ from arc_gate_mcp import ArcGateToolGuard
397
+
398
+ guard = ArcGateToolGuard(policy_mode="rag_assistant")
399
+
400
+ @mcp.tool()
401
+ async def read_document(path: str) -> str:
402
+ content = read_file(path)
403
+ return guard.check(content, tool_name="read_document")
404
+ """
405
+
406
+ def __init__(self, policy_mode: str = "balanced"):
407
+ self.policy_mode = policy_mode
408
+ self.blocked_count = 0
409
+ self.allowed_count = 0
410
+
411
+ def check(self, result: str, tool_name: str = "tool") -> str:
412
+ """
413
+ Check a tool result and return safe content.
414
+ Raises ValueError if blocked, returns sanitized content if restricted.
415
+ """
416
+ decision = _govern_tool_result(tool_name, result, self.policy_mode)
417
+
418
+ if decision.blocked:
419
+ self.blocked_count += 1
420
+ raise ValueError(
421
+ f"[Arc Gate] Tool result blocked — "
422
+ f"instruction-authority transfer detected in '{tool_name}'. "
423
+ f"Pattern: {decision.matched_pattern}"
424
+ )
425
+
426
+ if decision.decision == "restricted_continue":
427
+ self.blocked_count += 1
428
+ return (
429
+ f"[Arc Gate: Injected instructions removed]\n\n"
430
+ f"{decision.sanitized_content or result}"
431
+ )
432
+
433
+ self.allowed_count += 1
434
+ return result
435
+
436
+
437
+ # ── CLI entrypoint ────────────────────────────────────────────────────────────
438
+
439
+ def main():
440
+ import argparse
441
+
442
+ parser = argparse.ArgumentParser(
443
+ description="Arc Gate MCP — Runtime governance for MCP tool calls"
444
+ )
445
+ parser.add_argument(
446
+ "--upstream", required=True,
447
+ help="Upstream MCP server URL (e.g. http://localhost:8000/sse)"
448
+ )
449
+ parser.add_argument(
450
+ "--policy", default="balanced",
451
+ choices=["balanced", "browser_agent", "finance_agent", "rag_assistant", "strict"],
452
+ help="Policy mode (default: balanced)"
453
+ )
454
+ parser.add_argument(
455
+ "--transport", default="stdio",
456
+ choices=["stdio", "sse"],
457
+ help="Transport (default: stdio)"
458
+ )
459
+ parser.add_argument(
460
+ "--api-key", default=None,
461
+ help="Arc Gate API key (or set OPENAI_API_KEY env var)"
462
+ )
463
+ args = parser.parse_args()
464
+
465
+ proxy = ArcGateMCPProxy(
466
+ upstream_url=args.upstream,
467
+ policy_mode=args.policy,
468
+ api_key=args.api_key,
469
+ )
470
+ proxy.run(transport=args.transport)
471
+
472
+
473
+ if __name__ == "__main__":
474
+ main()
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: arc-gate-mcp
3
+ Version: 0.1.0
4
+ Summary: Runtime governance for MCP tool calls — Arc Gate for the MCP protocol layer
5
+ Author-email: Hannah Nine <9hannahnine@gmail.com>
6
+ License: AGPL-3.0
7
+ Project-URL: Homepage, https://bendexgeometry.com/gate
8
+ Project-URL: Repository, https://github.com/9hannahnine-jpg/arc-gate-mcp
9
+ Keywords: mcp,prompt-injection,ai-security,llm,agent,runtime-governance
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: mcp>=1.0.0
13
+ Requires-Dist: httpx>=0.25.0
14
+
15
+ # arc-gate-mcp
16
+
17
+ **Runtime governance for MCP tool calls.**
18
+
19
+ Arc Gate MCP sits between your agent and any MCP server. It intercepts all tool call results and enforces instruction-authority boundaries before the agent processes them.
20
+
21
+ When a tool result contains injected instructions — a poisoned document, a malicious webpage, a hostile database row — Arc Gate blocks them before they reach the agent.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install arc-gate-mcp
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### Full proxy (wraps any MCP server)
32
+
33
+ ```python
34
+ from arc_gate_mcp import ArcGateMCPProxy
35
+
36
+ proxy = ArcGateMCPProxy(
37
+ upstream_url="http://localhost:8000/sse",
38
+ policy_mode="rag_assistant",
39
+ )
40
+ proxy.run()
41
+ ```
42
+
43
+ ### Per-tool guard
44
+
45
+ ```python
46
+ from arc_gate_mcp import ArcGateToolGuard
47
+
48
+ guard = ArcGateToolGuard(policy_mode="rag_assistant")
49
+
50
+ @mcp.tool()
51
+ async def read_document(path: str) -> str:
52
+ content = read_file(path)
53
+ return guard.check(content, tool_name="read_document")
54
+ ```
55
+
56
+ ### CLI
57
+
58
+ ```bash
59
+ arc-gate-mcp --upstream http://localhost:8000/sse --policy rag_assistant
60
+ ```
61
+
62
+ ## Policy modes
63
+
64
+ | Mode | Behavior |
65
+ |---|---|
66
+ | `balanced` | Block on detected injection |
67
+ | `browser_agent` | Strip injections, allow safe content |
68
+ | `finance_agent` | Strictest — block everything suspicious |
69
+ | `rag_assistant` | Strip injections, preserve safe data |
70
+
71
+ ## Related
72
+
73
+ - [Arc Gate](https://github.com/9hannahnine-jpg/arc-gate) — OpenAI-compatible proxy version
74
+ - [arc-sentry](https://github.com/9hannahnine-jpg/arc-sentry) — Whitebox detector for self-hosted models
75
+
76
+ ## License
77
+
78
+ AGPL-3.0. Commercial license available — contact 9hannahnine@gmail.com.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ arc_gate_mcp/__init__.py
4
+ arc_gate_mcp/arc_gate_mcp.py
5
+ arc_gate_mcp.egg-info/PKG-INFO
6
+ arc_gate_mcp.egg-info/SOURCES.txt
7
+ arc_gate_mcp.egg-info/dependency_links.txt
8
+ arc_gate_mcp.egg-info/entry_points.txt
9
+ arc_gate_mcp.egg-info/requires.txt
10
+ arc_gate_mcp.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ arc-gate-mcp = arc_gate_mcp.arc_gate_mcp:main
@@ -0,0 +1,2 @@
1
+ mcp>=1.0.0
2
+ httpx>=0.25.0
@@ -0,0 +1 @@
1
+ arc_gate_mcp
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "arc-gate-mcp"
7
+ version = "0.1.0"
8
+ description = "Runtime governance for MCP tool calls — Arc Gate for the MCP protocol layer"
9
+ readme = "README.md"
10
+ license = { text = "AGPL-3.0" }
11
+ authors = [{ name = "Hannah Nine", email = "9hannahnine@gmail.com" }]
12
+ keywords = ["mcp", "prompt-injection", "ai-security", "llm", "agent", "runtime-governance"]
13
+ requires-python = ">=3.10"
14
+ dependencies = ["mcp>=1.0.0", "httpx>=0.25.0"]
15
+
16
+ [project.scripts]
17
+ arc-gate-mcp = "arc_gate_mcp.arc_gate_mcp:main"
18
+
19
+ [project.urls]
20
+ Homepage = "https://bendexgeometry.com/gate"
21
+ Repository = "https://github.com/9hannahnine-jpg/arc-gate-mcp"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["."]
25
+ include = ["arc_gate_mcp*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+