Skip to main content

MCP ZugaWatch

Runtime call-chain anomaly monitor for MCP servers. ZugaWatch watches what an MCP server actually does across a whole agent session — not just what a single tool definition says — and catches the emergent attacks that static scanners miss.

CI License: MIT Python 3.11+

Why this exists

There are already a dozen MCP static scanners — they read a server's tool definitions once and flag injection strings. But the real damage in agentic systems is emergent across calls: read a secret → POST it to a URL → delete the log. Each call looks fine alone. No open-source tool sequences the calls and flags the pattern. ZugaWatch does.

It also closes the rug-pull gap: a server passes review, then silently mutates its tool descriptions after install so the agent re-reads poisoned instructions next session. ZugaWatch cryptographically pins every tool definition and flags any post-approval change.

static scanners ZugaWatch
Scan tool definitions ✅ ✅
Cryptographic pin + rug-pull / drift detection ✗ ✅
Call-chain behavioural anomaly detection ✗ ✅
A–F grade per server some ✅
Ships as an MCP server (agent can self-audit) rare ✅
GitHub Action / CI gate some ✅

Install

pip install "git+https://github.com/Zuga-luga/zugawatch@v0.7.0"            # zero-dependency core (CLI)
pip install "git+https://github.com/Zuga-luga/zugawatch@v0.7.0#egg=zugawatch[server]"  # + the MCP-server entrypoint

pip install zugawatch (bare, from PyPI) is currently stuck on v0.1.0 (uploaded 2026-02-12) — six releases behind and missing pinning, the proxy, the static scanner, grading, and the GitHub Action entirely. The PyPI account that owns the name is locked out pending recovery; use the git-tag install above until that's resolved.

Use

1. Pin a server's tools, then detect rug-pulls

zugawatch pin examples/tools.json --lock zugawatch.lock   # trust on first use
zugawatch verify examples/tools.json --lock zugawatch.lock # later sessions
# DRIFT [mutated] get_weather   *** RUG-PULL SUSPECT ***   (exit 1)

2. Analyze a recorded call-chain

zugawatch analyze examples/chain_exfil.json
# [HIGH  ] SENT001  Data read by 'read_file' (seq 0) flows into network tool 'http_post' (seq 1) - possible exfiltration.
# [HIGH  ] SENT002  Destructive tool 'delete_file' (seq 2) runs after read 'read_file' (seq 0) across a server boundary - read-then-destroy pattern.

3. Grade it (CI gate — exit 0 for A/B, 1 otherwise)

zugawatch grade examples/chain_exfil.json
# GRADE D  (50/100)  findings=2 drifts=0   (exit 1)

4. Transparent proxy — record a live session automatically

Wrap any MCP server. ZugaWatch spawns it, relays stdio faithfully (the client and server don't know it's there), and records the real session — no manual JSON:

zugawatch proxy --lock zugawatch.lock --report report.json -- npx -y @some/mcp-server

Point your MCP client at zugawatch proxy -- <server cmd> instead of the server directly. On shutdown it writes a graded JSON report and prints a summary to stderr; tool-definition drift is flagged the moment tools/list comes back — the runtime rug-pull catch, before the agent uses the tools.

5. Statically scan a server's manifest (no execution)

Audit a server's published tool definitions for prompt-injection / tool-poisoning without ever running it — the safe way to vet untrusted servers at scale:

zugawatch scan manifest.json
# GRADE D  (50/100)  2 finding(s)
#   [HIGH  ] MCPP002  Tool 'add' description contains prompt-injection / override language.

Pull real manifests from a registry and scan them:

# Smithery serves real tool definitions (free key: smithery.ai/account/api-keys)
export SMITHERY_API_KEY=...
python fieldtest/fetch.py --source smithery --limit 200 --out fieldtest/servers.smithery.json
python fieldtest/run.py fieldtest/servers.smithery.json   # -> fieldtest/FINDINGS.md

# Glama is public but returns empty tools[] for most servers (verified June 2026)
python fieldtest/fetch.py --source glama --limit 200 --out fieldtest/servers.glama.json

Servers are never executed — the fetcher and scanner read published manifest JSON only, so this is safe to run across thousands of untrusted servers.

Field test: 183 live servers, naive scanners flag 401, we flag 0

I scanned 183 live public MCP servers (Smithery, 3,171 tool definitions). A keyword-style scanner — the common approach — flagged 32 servers with 401 findings. Every one was a false positive: password managers say "password," crypto tools say "token," prompt-engineering tools literally discuss "prompt injection." After tightening the rules from vocabulary to attack-patterns (and deleting two rules that proved unreliable on real data), the same 183 servers produced zero findings.

Detector (same 183 servers) Servers flagged Findings
Keyword rules (grep for scary words) 32 401 — all false positives
Attack-pattern rules (zugawatch) 0 0

The takeaway — in agent security, false-positive discipline is the whole game; a detector that cries wolf 401 times trains everyone to ignore it — is itself the result. Full method, caveats, and reproduction: fieldtest/WRITEUP.md.

6. GitHub Action — one-line CI gate

# .github/workflows/zugawatch.yml
- uses: Zuga-luga/zugawatch@v0.7.0
  with:
    tools: examples/tools.json        # rug-pull check (pins on first run)
    chain: examples/chain_exfil.json  # grade the recorded session

Fails the build on tool-definition drift or grade C-or-below, and writes the grade to the job summary. See examples/workflow.yml.

7. As an MCP server (agents self-audit)

zugawatch-mcp     # exposes analyze_chain, check_drift, grade_server

Built-in anomaly rules

ID Pattern Severity
SENT001 read-then-exfiltrate — read output flows into a later network call HIGH
SENT002 destructive-after-read — irreversible delete/overwrite following a read (HIGH across a server boundary) HIGH / MED
SENT003 repetition-loop — identical tool+args fired repeatedly (runaway agent) MED

Rules are plain functions (CallChain) -> list[Finding]; add your own by passing them to AnomalyEngine(rules=[...]).

Benchmark — measured, not claimed

Security tools live or die on data. ZugaWatch ships a labeled corpus (benchmark/dataset.py, 122 scenarios: attacks, benign, and evasion) and an evaluation harness (benchmark/run.py) that reports precision / recall / F1 / false-positive-rate. Run it yourself: python benchmark/run.py.

Metric Value
Precision 1.000
Recall 0.902
F1 0.948
False-positive rate 0.000
Latency p95 < 0.02 ms / scenario

Recall is deliberately not 1.0: the corpus includes a XOR-obfuscated exfiltration class that the current heuristics cannot see, and the harness reports it as a miss rather than hiding it. That recall ceiling is the roadmap. The rule improvements that took precision 0.887→1.000 and recall 0.855→0.902 (base64-aware data-flow; suppressing legitimate in-place file edits) were both driven by failures this benchmark surfaced. Full report: benchmark/RESULTS.md.

Design

agent ──calls──> [ ZugaWatch ] ──forwards──> target MCP server
                     │
                     ├─ pin tool defs on first connect (zugawatch.lock)
                     ├─ record every call into a CallChain
                     └─ run anomaly rules + grade

Capability tags (read / write / network / destructive) drive the rules. They come from MCP tool annotations (readOnlyHint, destructiveHint) and fall back to name/description heuristics when a server omits them — which most do.

Status

v0.7.0 — pinning, three call-chain anomaly rules, the static manifest scanner, grading, CLI, MCP-server interface, transparent stdio proxy, GitHub Action, an empirical benchmark, and a field-test harness (183 live servers, 401 false positives eliminated) are all implemented and tested (31 tests, CI-gated metrics).

Roadmap: defeat the XOR/encryption exfil evasion class (entropy + length heuristics), cross-server data-pivot and privilege-escalation rules, SARIF output, and PyPI publish.

License

MIT © Antonio Delgado

Metadata

Release files for zugawatch 0.7.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for zugawatch 0.7.1
File Size Uploaded Provenance Actions
zugawatch-0.7.1.tar.gz 38.7 kB With provenance Details for zugawatch-0.7.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for zugawatch 0.7.1
File Interpreter ABI Platform Actions
zugawatch-0.7.1-py3-none-any.whl Python 3 none any Details for zugawatch-0.7.1-py3-none-any.whl

Total release size: 61.3 kB

Release files / zugawatch-0.7.1.tar.gz

Download URL zugawatch-0.7.1.tar.gz
Size 38.7 kB
Tags Source
SHA-256 checksum
How to use checksums
4c985155ae39e755c659e7edf215bc254b6344a2677f0c3243d0699e0db8f97d
BLAKE2b-256 checksum
How to use checksums
cd2b1a143211ab6c89bdb22d54a7000de78af76138ebe1b9277ec2e8ca42cdab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

Release files / zugawatch-0.7.1-py3-none-any.whl

Download URL zugawatch-0.7.1-py3-none-any.whl
Size 22.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7782a397fff94b4126df387cc2f047324de714f9482df6bfe9d64db9499268d1
BLAKE2b-256 checksum
How to use checksums
f316fa98b0b6d2a3293a6e06efac6aabad3728528252c4625cf845d364676365
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

Security for zugawatch 0.7.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for zugawatch-0.7.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.7.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page