Skip to main content
Glama

MCP Security Proxy

A personal, on-demand red/blue/purple-team lab project. It logs every tool call an AI agent makes through an MCP proxy, baselines each agent's normal behavior, flags anomalies, runs a lab-scoped red-team simulation against that baseline, scores blue's detection rate, and narrates the whole run to Telegram.

Not a daemon. Nothing here runs 24/7 — it's triggered by a CLI command or a cron job, does one run, and exits. Targets are restricted to self-owned lab VMs only, never external systems.

Why

Most anomaly detection assumes the actor being profiled is a human on a network — peers, off-hours logins, transfer volume. This project applies the same behavioral baselining technique to a different actor: an AI agent's MCP tool calls. An agent that suddenly calls a tool it's never touched, at an hour it's never active, with a payload far outside its normal size, or with a burst of distinct tools in one window looks a lot like lateral movement looks on a network — so the same detection approach generalizes to it.

It's built on top of a prior project, lateral-movement-detector, which validated this baselining approach on real captured network traffic (per-device peer/hours/ volume/fan-out baselines, 4 signal types, 5/5 detection on a simulated SSH-flood attack with 0 false positives). This project ports that same detector logic (baseline.py / detector.py) onto a new data source — an MCP call log instead of a traffic capture — rather than starting the detection approach from scratch.

Related MCP server: agentic-observability-mcp

Architecture

AI agent (MCP client)
       │
       ▼
┌─────────────────┐     forwards every call     ┌────────────────────┐
│   proxy/server   │ ──────────────────────────▶ │ real downstream MCP │
│  (stdio MCP      │ ◀────────────────────────── │      server         │
│   proxy)         │        returns result        └────────────────────┘
└─────────────────┘
       │ writes one hash-chained entry per call
       ▼
logs/calls.jsonl  (proxy/audit_log.py — append-only, tamper-evident)
       │
       ▼
┌───────────────────┐   per-agent stats    ┌──────────────────┐
│ detector/baseline  │ ───────────────────▶ │ detector/baseline │
│  (learns normal)   │                      │     .json         │
└───────────────────┘                      └──────────────────┘
       │                                            │
       ▼                                            ▼
┌───────────────────────────────────────────────────────────┐
│ detector/detector — compares new calls to the baseline,    │
│ raises NEW_TOOL / OFF_PATTERN / PAYLOAD_OUTLIER /           │
│ SEQUENCE_SPIKE / UNKNOWN_AGENT, tiers by signal count        │
└───────────────────────────────────────────────────────────┘
       │
       ▼
┌───────────────────────────────────────────────────────────┐
│ detector/suppression — checks config/known_jobs.yaml;       │
│ strips signals a documented recurring job explains           │
│ (never silently — suppressed signals stay visible in output) │
└───────────────────────────────────────────────────────────┘
       │
       ▼  tier: alert / observation / suppressed

┌───────────────────┐        ┌───────────────────┐
│ red/orchestrator   │  scope │ config/lab_scope   │
│ picks technique +  │◀──────▶│  .yaml (hard        │
│ target, runs it via│        │  allowlist, fail-   │
│ red/executor        │        │  closed, exact-     │
│ (Mock, or Caldera   │        │  match only)         │
│ via --executor)     │        └───────────────────┘
└───────────────────┘
       │ injects the result as an MCP call under an
       │ EXISTING baselined agent identity
       ▼
purple/orchestrator — runs red, injects into logs/calls.jsonl,
runs detector.detect() once over the updated log, reads back
each round's outcome by trace_id, scores alert/observation/
suppressed/missed, logs to logs/purple_runs.jsonl
       │
       ▼
trigger/run.py — the on-demand entrypoint (CLI or cron). Drives
the same red+blue round-trip and narrates each stage via
trigger/telegram.py: run started → technique attempted →
detected/observation/suppressed/missed → containment
recommendation (alert tier only, trigger/containment.py) →
run summary
       │
       ▼
   Telegram chat

Every log (logs/calls.jsonl, logs/red_runs.jsonl, logs/purple_runs.jsonl) is append-only and hash-chained: each entry embeds the SHA-256 hash of the previous entry, so editing or deleting a prior line breaks the chain for everything after it. proxy/audit_log.py and red/run_log.py / purple/scoreboard.py are independent implementations of the same pattern rather than shared code, on purpose — different domain fields, low duplication cost, avoids coupling logs that evolve separately.

Components

Package

Role

proxy/

Stdio MCP proxy. Sits between the agent and a real downstream MCP server, forwards every call, writes a hash-chained audit entry (trace_id, UTC timestamp, agent_id, tool_name, target_resource, payload_size, reasoning_summary) for each one.

detector/

Learns a per-agent baseline (known tools, active hours, avg/std payload size, avg/std fan-out per time window) from the call log, then flags calls that deviate. Single signal → observation (logged only); 2+ correlated signals → alert.

detector/suppression.py

Checks anomalies against config/known_jobs.yaml — documented recurring legitimate patterns (agent + tool + hour window + payload range). Matched signals become suppressed, not silently dropped — the output still shows what was suppressed and by which job.

red/

Lab-scoped red-team simulation. scope.py is a fail-closed allowlist loader (missing/empty/wildcard scope = no targets, ever); executor.py's base class re-checks scope itself before any technique runs, so even a bug in the orchestrator can't get a live action dispatched out-of-scope; MockExecutor just reports what it would have done.

purple/

Runs a red-team round, injects the result into the same logs/calls.jsonl under an already-baselined agent identity (modeling "attacker compromised a trusted identity," not "attacker is a new identity"), runs the detector over the updated log, and scores the round as alert/observation/suppressed/missed.

trigger/

The on-demand entrypoint (trigger/run.py) meant for a manual command or a cron job. Drives one purple round-trip and narrates each stage to Telegram (trigger/telegram.py, falls back to stdout if no bot token/chat ID is configured). trigger/containment.py produces a plain-text recommendation for alert-tier detections only — never a real action.

Running it

Requires Python 3.12 and the packages in requirements.txt (a venv is expected — see PROJECT_SPEC.md's environment notes).

pip install -r requirements.txt

1. Generate some call traffic through the proxy (dev smoke test against the bundled mock downstream server):

python3 -m proxy.test_client

This drives proxy/server.py (configured via config/proxy_config.yaml), which appends entries to logs/calls.jsonl. Run it a number of times (or point a real MCP agent at the proxy) to build up enough history for a baseline.

2. Build the baseline:

python3 -m detector.baseline logs/calls.jsonl

Writes detector/baseline.json.

3. Run the detector standalone:

python3 -m detector.detector logs/calls.jsonl

Prints alerts / observations / suppressed calls against config/known_jobs.yaml.

4. Run a red-team simulation on its own (mock executor, targets restricted to config/lab_scope.yaml):

python3 -m red.orchestrator --runs 3

5. Run red + blue together and score detection:

python3 -m purple.orchestrator --rounds 5 --impersonate-agent dev-test-agent

Requires a baseline that already contains --impersonate-agent's identity (step 2 must have run against call history for that agent first).

6. Run the full on-demand pipeline with Telegram notifications:

cp .env.example .env   # fill in TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID, or leave blank
python3 -m trigger.run --rounds 5 --impersonate-agent dev-test-agent

Without Telegram credentials set, notifications print to stdout instead — the pipeline runs the same either way. For scheduled runs, add a cron entry, e.g.:

0 3 * * * cd /path/to/mcp-security-proxy && venv/bin/python3 -m trigger.run

Known limitations

Being upfront about where this project currently overstates or hasn't yet proven itself:

  • Live Caldera integration exists, but mock is still the default. red/executor.py now includes CalderaExecutor alongside MockExecutor, launching real MITRE Caldera operations (Discovery adversary, atomic planner) against a deployed agent — red/orchestrator.py --executor caldera opts in (MockExecutor runs by default). This has been verified end-to-end: a real operation against a real deployed agent, with actual whoami/passwd output captured back through the same scope-gated path as the mock. The known constraint, flagged directly in CalderaExecutor's docstring: Caldera targets an agent group, not an individual host, and this lab's scope (config/lab_scope.yaml) has exactly one host mapped to one fixed group — if the scope ever grows a second host on a different agent, CalderaExecutor needs a real target→group lookup, which doesn't exist yet.

  • target_resource and reasoning_summary are best-effort heuristics, not ground truth. MCP tool-call arguments are arbitrary JSON with no first-class "what resource is this touching" or "why" field. proxy/extract.py scans a fixed list of common argument key names (path, url, host, command, etc.) for target_resource, falling back to the first argument key/value if none match. reasoning_summary only reflects real agent intent if the calling agent populates the non-standard _meta.reasoning field this proxy defines; otherwise it's a generated stub listing the argument keys. Both are documented in the code as best-effort — the detector doesn't (and shouldn't) trust either one blindly.

  • Business-logic judgment isn't solved. config/known_jobs.yaml suppression only handles patterns a human has explicitly documented in advance (specific agent + tool + hour window + payload range). It does not, and isn't intended to, make any general judgment about whether a novel action is legitimate — an unmatched anomaly is always left for human review, never auto-cleared.

  • Detection has only been exercised against deliberately obvious anomalies. Verification so far (per project history) has been things like a brand-new tool name, an off-hours call, or a payload far outside normal range — synthetic and clearly outside the baseline. It has not been tested against a subtler or evasive adversary deliberately trying to blend into an agent's normal tool-call pattern (e.g. staying inside known tools/hours/payload ranges while still doing something malicious, or slowly drifting a baseline over many legitimate-looking calls before acting). The z-score/count-based signals here are not adversarially hardened.

  • Phase 7's tools_jsd drift threshold (0.15) is now calibrated, not guessed. detector/test_tools_drift_synthetic.py adds the test the other three thresholds already had: a synthetic agent whose read_file/list_dir usage mix drifts from ~95%/5% to a near-total reversal (~7%/93%) over four weeks, while staying inside known tools (no NEW_TOOL), a fixed hour set (no OFF_PATTERN), and a tool-independent payload distribution (no PAYLOAD_OUTLIER, and no payload/fan-out/hours contribution to BASELINE_DRIFT — the mix shift is isolated on purpose). A stable-weeks sanity check (two training weeks, both ~95/5 with ordinary day-to-day noise) measures the real tools_jsd noise floor at ~0.001 — roughly 150x below the 0.15 threshold, a much wider margin than hours_jsd has (0.4 vs. a measured floor of ~0.286, ~1.4x), because a skewed 2-tool mix is inherently far less noisy week-to-week than a 24-bucket hour-of-day histogram. Confirmed 0.15 as-is rather than lowering it: it stays silent through the early, still-ambiguous part of the drift (68.7%/31.3% mix → tools_jsd=0.094) and fires once the composition has genuinely inverted past its midpoint (49.7%/50.3% → 0.211), rising monotonically to 0.682 at full reversal — the two failure modes (false-positiving on ordinary variation, missing a real composition change) both check out for this scenario. Caveat: the measured floor is specific to a skewed two-tool baseline like the one tested; an agent with a more balanced multi-tool mix (e.g. three tools each used ~30–40% of the time) hasn't been measured and could plausibly have a higher natural noise floor — that generalization remains untested.

  • Other explicit non-goals carried over from PROJECT_SPEC.md: no least-privilege/access-control enforcement (trigger/containment.py only ever produces a human-facing recommendation, never a real action), and this isn't positioned as a competitor to commercial autonomous pentesting tools (XBOW, Pentera, etc.) — it's a smaller, specialized portfolio project.

Safety

red/scope.py is the single source of truth for what the red-team engine is allowed to target (config/lab_scope.yaml). It fails closed — a missing, empty, or wildcard/CIDR scope file yields no in-scope targets, never "allow everything" — and matching is exact-string only. red/executor.py's base class reloads and re-checks this file itself before any technique runs, independent of whatever the orchestrator computed, so a planning bug upstream can't get a live action dispatched to something not on the list. This is process-level, convention-based scoping, not network-level enforcement — it stops this codebase from acting out-of-scope, but it is not a substitute for real firewall/segmentation around the lab VM(s) it targets.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    A unified MCP server providing observability, safety control, and behavior evolution for high-agency AI agents through tracing, replaying, and auditing. It features real-time firewall guardrails and ML-driven anomaly detection to monitor, block, or fork agent actions based on risk.
    2 npm
    -
  • A
    license
    B
    quality
    C
    maintenance
    Enables deterministic security testing of AI agents that use tools by serving synthetic MCP environments with poisoned data, fake secrets, and privileged actions. Records agent tool calls and evaluates security invariants (e.g., canary leaks, forbidden access, approval binding) without an LLM judge or real systems.
    8
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Provides a secure MCP boundary for AI agents, intercepting and validating tool calls, redacting secrets, and requiring human approval for sensitive actions with a tamper-evident audit trail.
    -