Skip to main content
Glama
almohtadyg1

Agentrim MCP

by almohtadyg1
README.md
# Agentrim MCP

**A least-privilege enforcement proxy for MCP servers.**

[![CI](https://github.com/almohtadyg1/agentrim-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/almohtadyg1/agentrim-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-261230.svg)](https://github.com/astral-sh/ruff)
[![Type checked: mypy](https://img.shields.io/badge/type--checked-mypy%20strict-blue.svg)](https://mypy-lang.org/)
[![Tests: 86 passing](https://img.shields.io/badge/tests-86%20passing-brightgreen.svg)](tests/)

Agentrim MCP sits between an MCP client (an agent or LLM harness) and one
or more real upstream [Model Context Protocol](https://modelcontextprotocol.io)
servers. It builds and verifies a tool inventory offline, then enforces
least-privilege access to that inventory online: hiding denied tools
entirely, requiring human confirmation for risky ones, validating every
call against parameter constraints and rate limits, and logging everything
in a structured, greppable audit trail.

## Table of contents

- [How it works, in plain English](#how-it-works-in-plain-english)
- [Architecture](#architecture)
- [Provenance](#provenance-please-read-this)
- [Quickstart](#quickstart)
- [Installing](#installing)
- [Command-line reference](#command-line-reference)
- [Writing a policy](#writing-a-policy)
- [Running in production](#running-in-production)
- [Testing](#testing)
- [Evaluation](#evaluation)
- [Security](#security)
- [Troubleshooting](#troubleshooting)
- [Known issues and roadmap](#known-issues-and-roadmap)
- [Contributing](#contributing)
- [License](#license)

## How it works, in plain English

Think of an AI agent as a traveler, and the tools it can call (delete a
file, send an email, read a spreadsheet) as items the traveler wants to
carry onto a plane. agentrim-mcp is the security checkpoint standing
between the traveler and the gate.

- **Before anyone travels, security studies the manifest.** The offline
  extractor connects to the real server once, reads the full list of tools
  it offers, and writes down what each one does and how risky it looks
  (read-only, like reading a book, versus destructive, like detonating
  something). If a tool's description quietly changes later, that is
  flagged immediately: a tool that used to say "reads a file" suddenly
  saying "reads a file and emails it to a stranger" is exactly the kind of
  trick a hidden attacker would try, and it gets caught before anyone acts
  on it.
- **At the gate, the traveler only sees the lanes they are allowed to use.**
  Tools that are outright forbidden never even show up in the list the
  agent sees. An agent cannot be tricked into asking for something it
  never knew existed.
- **Every attempt to use something is checked, every single time.** An
  everyday, harmless action passes straight through. A risky action gets
  held for a human to approve first. A dangerous action is stopped
  automatically, no matter how the request is worded or how convincing the
  surrounding conversation sounds.
- **Everything is written in a logbook.** Every decision, allowed, denied,
  or held for approval, is recorded, so if something ever goes wrong there
  is a full paper trail to investigate.

That is the entire idea. The rest of this document is the engineering
detail behind those four bullet points.

## Architecture

```mermaid
flowchart TB
    subgraph Offline["Offline: run once per upstream server"]
        direction LR
        SA[static_analyzer.py] --> TC[trace_collector.py]
        TC --> RC[risk classifier]
        RC --> V[verifier.py]
    end

    V --> INV[(ToolInventory JSON<br/>drift-checked)]

    subgraph Online["Online: every agent request"]
        direction LR
        AF[adaptive_filter.py]
        VA[validator.py]
    end

    Agent["Agent / LLM<br/>MCP client"] <-->|MCP| PS[proxy_server.py]
    PS --> AF
    PS --> VA
    INV --> AF
    INV --> VA
    PS <-->|MCP| Upstream["Real upstream MCP server<br/>filesystem, memory, ..."]
    PS --> AL[audit_log.py<br/>JSON lines]
```

A single tool call looks like this:

```mermaid
sequenceDiagram
    participant Agent as Agent / LLM
    participant Proxy as agentrim-mcp
    participant Policy as Policy engine
    participant Upstream as Real MCP server

    Agent->>Proxy: tools/call delete_file
    Proxy->>Policy: evaluate(tool, arguments)
    Policy-->>Proxy: DENY, destructive tier
    Proxy-->>Agent: DENIED, with reason
    Note over Proxy,Upstream: Upstream is never contacted
```

Full design detail, including every judgment call made and why, is in
[`docs/architecture.md`](docs/architecture.md).

## Provenance, please read this

This is an independent engineering interpretation of the two-phase
architecture (offline tool extractor plus online tool orchestrator
enforcing least-privilege tool access via adaptive filtering and
status-aware validation, evaluated on AgentDojo) described in **AgenTRIM**
(arXiv:2601.12449, Betser, Bose, Giloni, Picardi, Padakandla and
Vainshtein, Fujitsu Research, submitted January 2026, under review). The
paper discloses that architecture at a conceptual level but not its exact
algorithms, thresholds, or scoring functions, and this project does not
claim to reproduce any of that. Every design decision that goes beyond
what the paper discloses (the risk classifier's keyword heuristics, the
policy YAML schema, the relevance-ranking algorithm, the sequence-anomaly
enforcement strength, and more) is original engineering work, tracked
claim by claim in [`docs/paper-mapping.md`](docs/paper-mapping.md).

This repository was also built and tested inside a sandboxed environment
with no live LLM API access. Every result described as "real" below
(extraction against live servers, test pass counts, evaluation numbers)
was actually run during development; see [`PROGRESS.md`](PROGRESS.md) for
the phase-by-phase log and [`KNOWN_ISSUES.md`](KNOWN_ISSUES.md) for the
real bugs live testing surfaced. The shipped evaluation numbers are an
explicitly-labeled illustrative synthetic run, not a live AgentDojo
benchmark against an LLM; see [Evaluation](#evaluation) below.

## Quickstart

```bash
git clone <this-repo> agentrim-mcp && cd agentrim-mcp
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

# 1. Cache the official filesystem MCP reference server once (needs npm registry access).
npx -y @modelcontextprotocol/server-filesystem --help

# 2. Find its resolved entrypoint. Invoking node directly avoids a real npx
#    stdio-interaction bug documented in KNOWN_ISSUES.md #1.
ENTRYPOINT=$(find "$(npm config get cache)/_npx" -path "*server-filesystem/dist/index.js" | head -1)

# 3. Extract and verify a tool inventory for a sandbox directory.
mkdir -p /tmp/agentrim-sandbox
agentrim extract --server-name filesystem --command node \
    --args "$ENTRYPOINT /tmp/agentrim-sandbox" \
    --output /tmp/fs_inventory.json
agentrim verify /tmp/fs_inventory.json

# 4. Serve the proxy in front of the real server.
agentrim serve --inventory-path /tmp/fs_inventory.json \
    --policy-path src/agentrim/policy/default_policy.yaml \
    --upstream-command node \
    --upstream-args "$ENTRYPOINT /tmp/agentrim-sandbox" \
    --audit-log-path /tmp/agentrim_audit.jsonl
```

Point your MCP client's stdio transport at `agentrim serve` (instead of
directly at the upstream server) and it now enforces the policy in
`default_policy.yaml`: read-only tools allowed, writes require confirmation
through the `agentrim_confirm` meta-tool, destructive or unclassified tools
denied. See [`examples/wrap_filesystem_server.py`](examples/wrap_filesystem_server.py)
and [`examples/wrap_memory_server.py`](examples/wrap_memory_server.py) for
runnable, end-to-end programmatic versions of the same flow; both were run
live against the real reference servers during development.

Every step above completes in well under five minutes on a machine with
npm registry access. The only slow step is the one-time package cache
warm (roughly 10 to 20 seconds); everything after is near instant.

## Installing

```bash
pip install -e ".[dev]"     # core package plus test and lint tooling
pip install -e ".[eval]"    # adds agentdojo, for the evaluation harness
```

Requirements:
- Python 3.10 or newer.
- `agentrim serve`/`extract` launch upstream MCP servers as subprocesses
  (for example via `node`, `npx`, or any other command), so whatever
  runtime the upstream server needs (Node.js, Go, and so on) must also be
  installed separately. Most official MCP reference servers need Node.js
  18 or newer.

### Docker

A `Dockerfile` is included for containerized deployment:

```bash
docker build -t agentrim-mcp .
docker run --rm -it \
    -v "$(pwd)/data:/data" \
    -v "$(pwd)/logs:/var/log/agentrim" \
    agentrim-mcp \
    agentrim serve \
      --inventory-path /data/inventory.json \
      --policy-path /app/config/example.policy.yaml \
      --upstream-command node \
      --upstream-args "/data/upstream-server/index.js /data" \
      --audit-log-path /var/log/agentrim/audit.jsonl
```

Note: the Dockerfile has been reviewed for correctness but has not been
build-tested in this project's own development environment, since Docker
was not available there. Please verify it builds in yours before relying
on it; see `KNOWN_ISSUES.md` for the full note.

## Command-line reference

All commands are available via the `agentrim` entrypoint once installed.

### `agentrim extract`

Connects to a live upstream MCP server, runs the offline extractor, and
writes a verified tool inventory.

| Flag | Required | Description |
|---|---|---|
| `--server-name` | yes | Logical name for the upstream server, stored in the inventory. |
| `--command` | yes | Command used to launch the upstream server, for example `node`. |
| `--args` | no | Space-separated arguments for that command. |
| `--trace-file` | no | Path to a JSONL execution-trace log to augment the inventory with observed call sequences. |
| `--output` | no | Path to write the inventory JSON (default `inventory.json`). If a file already exists there, it is used as the previous inventory for drift detection. |

### `agentrim verify`

```bash
agentrim verify path/to/inventory.json
```

Prints a table of every tool in a previously extracted inventory, its risk
tier, and whether it is currently flagged for description or schema drift.

### `agentrim serve`

Starts the orchestrator proxy: connects to the real upstream server over
stdio and serves a filtered, validated MCP server over stdio to the
downstream client.

| Flag | Required | Description |
|---|---|---|
| `--inventory-path` | yes | Path to a verified inventory JSON (from `extract`). |
| `--policy-path` | yes | Path to a policy YAML file. |
| `--upstream-command` | yes | Command used to launch the upstream server. |
| `--upstream-args` | no | Space-separated arguments for that command. |
| `--max-visible-tools` | no | Cap on how many tools are shown per `tools/list` after relevance ranking. |
| `--audit-log-path` | no | Path to write JSON audit log lines; if omitted, logs go to stdout. |

### `agentrim report`

```bash
agentrim report path/to/audit.jsonl
```

Summarizes an audit log: total validated calls, counts by verdict,
drift-related denials, and how many soft sequence anomalies were logged.

## Writing a policy

Policies are explicit, human-editable YAML with a structurally-enforced
default-deny posture: `Policy.default_action` cannot be set to `allow`,
the schema itself raises on load if you try. See
[`src/agentrim/policy/default_policy.yaml`](src/agentrim/policy/default_policy.yaml)
for the minimal default this repo ships with, and
[`config/example.policy.yaml`](config/example.policy.yaml) for a fuller,
annotated example covering parameter constraints, rate limits, and glob
rules, including a documented pitfall around rule ordering.

Minimal shape:

```yaml
version: 1
default_action: deny   # only "deny" or "confirm" are ever valid here

risk_tier_defaults:
  read_only: allow
  write: confirm
  destructive: deny
  unknown: deny

rules:
  - tool: "read_file"
    action: allow
    param_constraints:
      - param: "path"
        deny_patterns: ["\\.\\."]   # blocks path traversal, checked against
                                     # both the raw and URL-decoded value

global_rate_limit_per_minute: 120
```

## Running in production

A few practical notes beyond the Quickstart:

- Run `agentrim extract` on a schedule (a cron job or CI job) against your
  production upstream servers, and diff the resulting inventory against
  the previous one before deploying it; `agentrim verify` and the
  `drifted` column it prints are the signal to look at.
- Point `--audit-log-path` at a durable, rotated log destination. The
  audit logger writes one JSON object per line, so it is directly
  consumable by `jq`, a log shipper, or `agentrim report`.
- Treat the policy YAML as configuration that goes through the same
  review process as code; it is the actual security boundary.
- The proxy currently tracks one logical session per process (see
  `docs/architecture.md`'s Roadmap for a future multi-session store), so
  run one `agentrim serve` process per agent connection in a multi-agent
  deployment.

## Testing

```bash
pytest -v --cov=agentrim --cov-report=term-missing
ruff check src/ tests/ evaluation/ examples/
ruff format --check src/ tests/ evaluation/ examples/
mypy src/
```

86 tests, all passing, at roughly 93 percent statement coverage on
`src/agentrim` as of the last run; `ruff` and `mypy --strict` are both
clean. Coverage is weakest in `cli.py` (around 76 percent), mostly the
`serve` command's live-process wiring, which is exercised by the example
scripts run manually rather than by unit tests. `tests/test_stress.py`
covers scale (2000-tool inventories, 10,000-line trace files), unicode
tool names and arguments, exact rate-limit boundaries, and a range of
malformed policy documents.

One CLI test and both `examples/` scripts run live against the real,
official `@modelcontextprotocol/server-filesystem` and
`@modelcontextprotocol/server-memory` reference servers when `node` and
those packages are available locally; they skip gracefully otherwise, so
no network access is required to run the rest of the suite.

## Evaluation

[`evaluation/agentdojo_runner.py`](evaluation/agentdojo_runner.py) is a
real, tested integration with the actual
[AgentDojo](https://github.com/ethz-spylab/agentdojo) package
(`agentdojo==0.1.35`): it converts a real AgentDojo task suite's tools into
an agentrim-mcp `ToolInventory` and wraps `FunctionsRuntime` so every call
is validated by the same policy engine the MCP proxy uses.

[`evaluation/baseline_vs_agentrim.py`](evaluation/baseline_vs_agentrim.py),
run for real during development, loads the actual official AgentDojo
`v1.1.1` `workspace` suite and its real environment, and runs a small,
deterministic, non-LLM mock agent through three synthetic
indirect-prompt-injection scenarios, with and without agentrim-mcp in
front. This development environment has no live LLM API access, so this is
explicitly an illustrative synthetic run, not a live AgentDojo benchmark;
see the label in its own JSON output and `docs/paper-mapping.md` for why.
The real result of that real run:

| | Task Completion Rate | Attack Success Rate |
|---|---|---|
| Baseline, no agentrim-mcp | 1.00 | 1.00 |
| With agentrim-mcp | 1.00 | 0.00 |

That is, the legitimate task still completes, and every
injected-destructive-call attempt this scenario set models is blocked.
Full output is in
`evaluation/results/illustrative_baseline_vs_agentrim.json`.
[`.github/workflows/eval.yml`](.github/workflows/eval.yml) is ready for a
maintainer with real LLM API credentials to run an actual AgentDojo
benchmark.

## Security

See [`SECURITY_REVIEW.md`](SECURITY_REVIEW.md) for an adversarial review
that found and fixed a real path-traversal-encoding bypass, a real
confirmation-token replay gap, and hardened the validator to fail closed
on unexpected internal errors, all verified with tests rather than
reasoned about in the abstract. This is a portfolio and reference project
rather than a monitored production service; see
[`CONTRIBUTING.md`](CONTRIBUTING.md) for how to report a further finding.

## Troubleshooting

- **`npx`-launched upstream servers crash or hang.** Invoke `node` directly
  on the resolved entrypoint instead of going through `npx`; see
  `KNOWN_ISSUES.md` #1 for the root cause.
- **A read-only-looking tool got classified as `write` or `destructive`.**
  The risk classifier is a transparent keyword heuristic, not a semantic
  model; check `src/agentrim/risk/risk_tags.py` for the exact keyword
  lists and override the classification with an explicit rule in your
  policy YAML if needed.
- **A tool I expected to be visible is missing from `tools/list`.** Check
  whether it resolved to `deny` under your policy (denied tools are
  removed from the list entirely by design, not merely blocked at call
  time); `agentrim verify` and the audit log will show the policy
  decision.
- **A confirmation token stopped working.** Tokens expire after
  `ProxyConfig.confirmation_ttl_seconds` (300 seconds by default); request
  the call again to get a fresh token.

## Known issues and roadmap

See [`KNOWN_ISSUES.md`](KNOWN_ISSUES.md) for real bugs found and fixed
during development, including risk-classifier false positives found by
running against live reference servers and AgentDojo's real tool set, and
[`docs/architecture.md`](docs/architecture.md)'s Roadmap section for what
was deliberately deferred rather than gold-plated for v1: semantic
relevance ranking, a distributed session store, and a review dashboard.

## Contributing

See [`CONTRIBUTING.md`](CONTRIBUTING.md) for setup instructions, the
pre-commit hooks, and the standard this project holds itself to for new
changes.

## License

MIT, see [`LICENSE`](LICENSE). Chosen for maximum compatibility with the
MCP Python SDK and the broader MCP server ecosystem this project wraps,
and because a permissive license suits a reference and portfolio security
tool meant to be freely adapted.