Metadata-Version: 2.4
Name: mcp-ztgateway
Version: 0.3.0
Summary: Zero-trust security gateway for MCP (Model Context Protocol) tool servers — verifies a tool's declared capabilities against observed kernel syscalls, then confines it per call with mounts, seccomp and network namespaces.
License: MIT
Project-URL: Homepage, https://github.com/nabrahma/MCP_Zero-Trust_Gateway_BTP
Project-URL: Repository, https://github.com/nabrahma/MCP_Zero-Trust_Gateway_BTP
Project-URL: Issues, https://github.com/nabrahma/MCP_Zero-Trust_Gateway_BTP/issues
Keywords: mcp,model-context-protocol,security,zero-trust,gateway,llm,agent,sandbox,seccomp,landlock,prompt-injection,tool-poisoning,capability,least-privilege
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Framework :: FastAPI
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.115
Requires-Dist: uvicorn>=0.30
Requires-Dist: mcp[cli]>=1.0
Requires-Dist: pydantic>=2.0
Requires-Dist: PyYAML>=6.0
Requires-Dist: click>=8.0
Requires-Dist: httpx>=0.27
Requires-Dist: jsonschema>=4.20
Provides-Extra: audit
Requires-Dist: pip-audit>=2.7; extra == "audit"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Dynamic: license-file

# MCP Zero-Trust Gateway

A security gateway between an LLM agent and the MCP (Model Context Protocol) tool
servers it uses, built on one principle: **an MCP server's manifest is
attacker-controlled and therefore unverifiable, but its syscalls are not.**

Every tool is inspected before admission, profiled under `strace` in a hardened
container to see what it *actually does*, checked against what it *claimed*, and
then confined per call at the kernel level. It runs entirely on your machine —
the trust store is a local SQLite file and no telemetry leaves the host.

---

## The problem

An agent discovers tools by reading each server's self-description
(`tools/list`), then calls them with its own privileges. It treats that
description as true.

Three things follow, and each is a real attack:

- A description is **instruction-adjacent text inside the model's context**, read
  on every turn. Hide instructions there and the agent obeys them.
- A tool response **re-enters the agent's reasoning as a trusted observation**.
  Whoever controls retrieved content controls the agent's next move.
- Tools are installed like packages but run with **ambient host authority** —
  your files, your environment variables, your network.

## What this does about it

**Declare → verify → confine.** A tool publishes a capability declaration; kernel
syscall profiling verifies it before admission; per-tool resource grants enforce
it at runtime. An attacker must either *declare the attack* — and be confined to
it — or be *caught exceeding the declaration*.

Comparable systems each hold one half of that loop: `sandlock.mcp` confines
against declarations it never checks, SandScope observes flows it cannot stop,
mcp-sec-audit inspects source it never runs, ClawGuard authorises calls it cannot
contain.

---

## Results

Measured against real containers, real MCP servers and real `strace`. Every
figure is traceable to a committed row — [`eval_results.csv`](eval_results.csv),
[`evidence/`](evidence/).

| Corpus | Result | n |
|---|---|---|
| Runtime DSR (Appsecco + rug-pull + evasion) | **84.6%** (11/13) | 13 |
| ↳ Phase 1 alone (Appsecco) | 81.8% (9/11) | 11 |
| **Containment** (purpose-built corpus) | **87.5%** (7/8) | 8 |
| **Declaration verification** | **100% / 100%** (detection / fidelity) | 5 |
| MCPTox (detection only — see caveat) | **88.0%** (425/483) | 483 |
| MSB (detection only, small n) | 100% (5/5) | 5 |
| False positives | **25–50%** (varies by run) | 4 |
| Inconclusive | **0** | — |
| Unit tests | **389 passing**, 29 skipped | — |

Reference points: ClawGuard 89.50% MCPTox / 94.21% MSB; mcp-sec-audit 74.7%
MCPTox and **0% on JavaScript servers**.

> **Read the caveats before quoting any of this.** MCPTox is *not* measured the
> way ClawGuard measures it — we start no agent and no container, and 346 of our
> 425 defences there are regex over manifest text. The false-positive rate is not
> stable. Two of the three runtime phases are n = 1. All of it is spelled out in
> [`MD_files/Paper/`](MD_files/Paper/README.md).

**Determinism:** across five consecutive runs from a wiped trust store,
**503 of 503 scored rows produced identical verdicts**. The one varying metric is
the false-positive rate, and the cause is a tool whose own probe fires
inconsistently — not the gateway.

---

## Architecture

Two phases sharing one SQLite trust store; they never call each other.

### Onboarding — `POST /register`, once per server

1. **L1 static scan** — 43 rules over names, descriptions and schemas.
   Three-valued: an attack denies, something merely suspicious quarantines.
2. **L2 live profiling** — the server runs under `strace -f -yy -ttt` inside
   `docker run --cap-drop ALL --read-only`, driven over real MCP. Up to 8 tools
   are exercised with arguments synthesised from each tool's own `inputSchema`.
   `strace` is injected automatically if the image lacks it, so Node, Python and
   Go servers all profile without special-casing.
3. **Capability mapping** — syscalls → `{network, filesystem, subprocess, env}`,
   split at the MCP `initialize` boundary so package-manager bootstrap is not
   blamed on the tool.
4. **L3 comparator + declaration verification** — observed vs declared. A tool
   that exceeds its own published grant is denied, and no renaming evades it.
5. **Seccomp compilation** — the capability *verdict* (not the raw trace) is
   compiled into a syscall deny-list by subtraction, then **smoke-tested**: the
   tool must complete a real MCP `initialize` under it, or the gateway degrades a
   tier and says so.

### Runtime — `POST /tool_call`, every call

Inbound DLP → trust-store lookup → **L4 policy** (three-valued: allow / deny /
*hold for approval*) → **response-provenance gate** → fresh confined container →
**manifest re-attestation** → outbound DLP → log.

Each call gets only the paths its verified grant names, only the environment
variables it declared, `--network none` unless it declared hosts, and its own
seccomp profile.

### What is novel here

- **Response-provenance gating** — asks *where an argument came from* rather than
  what it means, so it is paraphrase- and encoding-proof.
- **Continuous manifest attestation** — catches a description that changes
  *after* approval (CVE-2025-54136 class), separating "changed" from "changed
  into an attack".
- **Declaration verification as a measurable axis** — comparable systems score 0%
  on it by construction.

---

## Scope: process ownership

Kernel-level protection requires the gateway to be **the process that spawns the
tool**.

- **Supported** — any MCP server the gateway launches as a child process:
  effectively all `stdio` tools (`npx`, `python`, compiled binaries).
- **Unsupported** — anything running remotely, or launched by another
  application. A native desktop app exposing SSE on `127.0.0.1` cannot be
  profiled, because we did not spawn it.

This is the inverse of protocol-layer defences like ClawGuard, which inspect
JSON-RPC and work on servers they never own but cannot constrain execution. The
two are complementary, with inverse blind spots.

---

## Install

Docker Desktop is required for L2 profiling.

```bash
pip install mcp-ztgateway
mcp-ztgateway --port 8000
```

From source:

```bash
git clone https://github.com/nabrahma/MCP_Zero-Trust_Gateway_BTP.git
cd MCP_Zero-Trust_Gateway_BTP
python -m venv venv && ./venv/Scripts/activate
pip install -r requirements.txt
./start_gateway.ps1
```

---

## Usage

### Onboard a tool

```json
POST /register
{
  "name": "read_file",
  "command": ["docker","run","-i","--rm","real-mcp-image","mcp-server-filesystem","/app"],
  "image": "real-mcp-image",
  "test_tool": "read_file",
  "test_args": {"path": "/app/package.json"}
}
```

Returns a verdict — `allow`, `allow_with_warning`, `needs_declaration`,
`quarantine` or `deny` — plus the observed capabilities, the compiled seccomp
profile, and a **paste-ready declaration** drafted from what the tool was
actually seen doing.

`needs_declaration` means "we could not classify this tool, so we have no basis
to judge it". It is non-runnable like quarantine, but it is *not* an accusation.

### Declare capabilities (recommended)

```json
"annotations": {
  "mcpzt:fs_read":         ["/workspace"],
  "mcpzt:net_allow_hosts": ["api.example.com"],
  "mcpzt:subprocess":      ["node"],
  "mcpzt:env":             ["WORKSPACE"]
}
```

Standard MCP hints (`readOnlyHint`, `openWorldHint`) are deliberately **ignored**
— they are claims by the tool author, and honouring them would let a malicious
server grant itself anything.

### Scope a session to a task (optional)

```json
POST /session   { "session_id": "s1", "objective": "Read ./report.md and summarise it" }
```

Calls outside that objective are **held for approval**, not denied. Approve with
`POST /approvals/{id}`; the approval authorises the exact arguments a human saw
and is consumed on use.

### Other endpoints

`/logs` · `/verdicts` · `/profiles` · `/enforcement` · `/egress` ·
`/confinement` · `/approvals`

### Docker Compose

```bash
docker-compose up --build     # gateway :8000, dashboard :5173
```

> Live profiling needs the host Docker daemon. The compose image deliberately has
> **no Docker socket**, so run the gateway on the host for evaluation.

---

## Limitations

Stated properly in
[`MD_files/Paper/LIMITATIONS.md`](MD_files/Paper/LIMITATIONS.md). The ones that
matter most:

- **Response-content attacks are not covered.** A tool that does its advertised
  job and returns *fabricated data* crosses no boundary here. This is the open
  frontier, and every comparable system shares it.
- **Dynamic profiling is evadable and non-deterministic.** A dormant payload is
  invisible at onboarding; containment is what makes that survivable.
- **Corpora are small.** No statistical significance is claimed anywhere.

---

## Documentation

Start at [`MD_files/README.md`](MD_files/README.md).

| Want to… | Read |
|---|---|
| Join the project | [TEAMMATE_HANDOVER.md](MD_files/Planning_and_Management/TEAMMATE_HANDOVER.md) |
| Write the paper | [`MD_files/Paper/`](MD_files/Paper/README.md) |
| Check a number | [Paper/RESULTS.md](MD_files/Paper/RESULTS.md) |
| Review the design | [ARCHITECTURE.md](MD_files/Architecture_and_Design/ARCHITECTURE.md) |
| Reproduce results | [Paper/REPRODUCIBILITY.md](MD_files/Paper/REPRODUCIBILITY.md) |

**How to read our numbers.** Every defence names the layer that produced it.
Crashes and timeouts are `INCONCLUSIVE` and stay in the denominator — never
counted as defences. DSR is never quoted without the false-positive rate. All raw
data is committed, so if a number looks too good, open the row and check it.

That discipline exists because this project once reported a **100% Defence
Success Rate that was an artefact of two measurement bugs cancelling out** — a
seccomp compiler that made every container fail to boot, and a harness that
scored the crashes as defences. The post-mortem is
[GROUND_TRUTH_AUDIT.md](MD_files/Audits_and_Evaluations/GROUND_TRUTH_AUDIT.md).

---

## Tech stack

Python · FastAPI · official MCP SDK (stdio) · Docker · `strace` · seccomp ·
Landlock (opt-in) · SQLite · React dashboard

## License

MIT — see [LICENSE](LICENSE).
