Releases: churik5/bulwark-mcp
Release list
v0.4.2
v0.4.1
Changed
- Renamed project:
mcp-firewall→bulwark-mcp. PyPI name conflict
with an unrelated project of the same name (different architecture:
OPA / Rego, RBAC, compliance). Discovered before the public launch.
See ADR-0006.- PyPI distribution name:
bulwark-mcp(wasmcp-firewall). - CLI command:
bulwark(wasmcp-firewall). - Python module / import name:
bulwark_mcp(wasmcp_firewall). - GitHub repository:
churik5/bulwark-mcp(waschurik5/mcp-firewall). - Environment variables:
BULWARK_TELEMETRY,BULWARK_TELEMETRY_URL,
BULWARK_DB,BULWARK_CONFIG(wereMCP_FIREWALL_*).
- PyPI distribution name:
- No functional changes. Same code, same behaviour, same versioned schemas.
Migration from v0.4.0
pip uninstall mcp-firewall # if you installed the prior wheel
pip install bulwark-mcp # once published
# CLI: replace `mcp-firewall <cmd>` with `bulwark <cmd>` everywhere.
# Env vars: rename MCP_FIREWALL_* → BULWARK_* in your shell rc / systemd unit.
# Existing data/log.db opens unchanged; schema stays at v2.v0.4.0
Added
mcp-firewall doctor— environment diagnostic with four checks
(Python version, Ollama reachable + model loaded, audit DB writable
at schema v2, rules + policy validate). Exit code reflects worst
status: 0 = pass, 1 = warn, 2 = fail.mcp-firewall benchmark— three workloads (rules detector,
inspector cache hit, end-to-end via cat) with p50/p95/p99 output.- GitHub Actions workflows for the launch:
publish.yml— PyPI OIDC trusted publishing on tag.test-publish.yml— manual test.pypi.org publishing.release.yml— auto release notes extracted from CHANGELOG.sync-labels.yml— applies.github/labels.ymlon push.auto-label.yml— keyword-based labels on new issues.welcome.yml— first-time contributor greeting (issues + PRs).stale.yml— closes inactive issues / PRs after 90 d with a
7-d warning.- All workflows opt-out via repo variable
MCP_FIREWALL_DISABLE_<NAME>.
docs/FAQ.md— full 10-question FAQ; top three inlined into
README.docs/PERFORMANCE.md— measured latency + community-data table
populated bymcp-firewall benchmark.docs/RELEASING.md— release procedure relying on PyPI trusted
publishing (no token in repo).- PyPI metadata polish — extra classifiers,
Changelog/
Documentation/Release notes/Security policyURLs,
maintainer email.
Changed
- README hybrid rewrite — radical voice on hero, problem statement,
top-3 FAQ; light polish on technical sections. Removed feature-bullet
log in favour of a problem-first opener. - Rules detector — three-pass scan now also folds Cyrillic and
Greek homoglyphs into Latin counterparts before regex matching, so
payloads usingІgnore(Cyrillic) orιgnore(Greek) fire
role_hijack.ignore_previous. - Health endpoint — per-connection
wait_for(timeout=5s)plus
StreamReader limit=8KiBclose the slowloris path. Snapshot is
cached for 1 s under anasyncio.Lockso a probe storm does not
trigger a SCAN every request. - Stats / telemetry —
det_rulesJSON parse capped at 64 KiB per
row to bound CPU on a corrupted audit log. - Batch JSON-RPC handling — when any member of a batch blocks,
the proxy now emits a per-id reply array (one response per request
id), not a 1-element substitute. Benign s2c siblings forwarded
verbatim; benign c2s siblings get a synthesised
-32099 batch_aborted_by_siblingerror reply.
Security
- ReDoS guard for community regex —
mcp-firewall rules lint --strictrejects patterns that contain a nested quantifier
((a+)+,(.*x)*) and any pattern that takes more than 100 ms
on a 512-char benign probe (SIGALRM-bounded). - Batch JSON-RPC ID confusion fixed — see Changed above.
- Health endpoint slowloris — hostile localhost peer can no
longer hold an event-loop slot indefinitely. - Cross-script homoglyph evasion closed — Cyrillic / Greek
look-alikes for Latin letters fold during normalisation.
Known limitations (v0.5 backlog)
- HTTP/SSE transport.
- Community rules repository (
mcp-firewall/rules-community). - Viewer filters in
mcp-firewall logs(search by rule id, by
trace id). - Anthropic Haiku fallback tier for the LLM classifier.
- Full Unicode
confusables.txtshipped as data — current homoglyph
table is hand-picked from published PoCs.
v0.3.0 - Community readiness (integrations, observability, hardening)
mcp-firewall v0.3.0 — Community readiness, observability, audit-fix harvest
Date: 2026-05-05
Status: alpha — opt-in detector and telemetry; off-by-default posture preserved.
Headline
mcp-firewall is now ready for the public OSS launch. Three things changed:
- Audit-fix harvest from v0.2. The five deferred findings from the Week-2 self-audit are landed: NFKC + invisible-char three-pass scan, per-member inspection of JSON-RPC batch frames, explicit
skipped:non_text_contentaudit note, one-end truncation closing the seam evasion, and theHaiku fallbackdeferred to v0.4 with an explicit ADR slot. - Observability without compromising the privacy posture: a local
statscommand, a loopback/healthendpoint, and an opt-in telemetry pipe that ships nothing more than version + OS + event counts. - Community readiness —
CONTRIBUTING.md(with the rule-pack promotion ladder),SECURITY.md(GitHub Security Advisories), three integration-tested MCP servers (github,brave-search,postgres).
# Try the new commands:
mcp-firewall stats --since 24h
mcp-firewall stats --json --compact
mcp-firewall rules lint --strict src/mcp_firewall/rules/builtin/
mcp-firewall run --server "..." --health-port 8765What's new
Stats (local-only)
mcp-firewall stats is a read-only roll-up of the audit log. Rich-table by default, JSON via --json (pretty by default; --compact for one-line). Window selector --since 7d|24h|30m. Versioned schema (schema_version: 1) so future changes are non-breaking.
Health endpoint
--health-port N binds a tiny asyncio listener on 127.0.0.1:N and serves one route: GET /health → 200 application/json. For k8s liveness probes and docker HEALTHCHECK. Loopback-only by design — no auth, no TLS, no external exposure.
Telemetry — opt-in, anonymous, transparent
- Off by default. Only the env var
MCP_FIREWALL_TELEMETRY=trueenables it. - Privacy contract. No rule names, no method names, no traffic content, no IPs/hostnames. Only
installation_id(a self-mintable UUID), version, OS family, Python version, and four integer event counts. - Local log first. Every payload is appended to
data/telemetry.logbefore the HTTP call. Network errors never erase the log entry. - Endpoint kill-switch.
MCP_FIREWALL_TELEMETRY_URL=disabledskips the HTTP call but still writes the local log. - Files mode 0600. Co-tenants on a shared machine can't read payloads.
- Full schema and "what we DON'T send" in
docs/OBSERVABILITY.md.
Rules lint
mcp-firewall rules lint <path> validates community-contributed YAML packs:
- Basic mode: syntax, regex compilation, valid
apply_to, score in[0.0, 1.0]. Equivalent to load-time validation. --strict: +description ≥ 10chars +sourceis an HTTP(S) URL +severity_tierset +attack_exampleslist with at least one entry that actually matches the regex +false_positive_examples(when present) must NOT match.
The strict bar is the gate for promotion from community/ to built-in/ (see CONTRIBUTING.md).
Audit-fix harvest
| Finding (Week 2) | v0.3 fix |
|---|---|
| NFKC normalisation | Three-pass scan: raw + within-word + between-word normalised |
| Batch frame per-member inspection | Per-member rows in audit log; whole-batch block on any hit |
| Non-text content shapes | note=skipped:non_text_content in audit |
| Truncation seam | One-end (head-only) truncation; note=ok:truncated=<chars> |
| Haiku fallback | Deferred to v0.4 (async-parallel inspection redesign) |
Privacy hardening (v0.3 audit findings)
The Week-3 self-audit surfaced 4 high/medium issues already closed in this release:
platform.release()reduced to its first numeric component (was leaking custom kernel build strings).data/telemetry.loganddata/installation_idwritten with mode0600._trace_idno longer mixes the raw frame into the SHA1 seed (os.urandom(8)is sufficient and faster).- LLM truncation is now visible in audit via
note=ok:truncated=<chars>.
Full report: docs/AUDIT-REPORT-week3.md.
Tested MCP integrations
| Server | Threat the integration test asserts the proxy catches |
|---|---|
github-mcp-server |
Role-hijack + exfiltration text inside an issue body |
brave-search-mcp |
Search-snippet poisoning (page text steers the agent) |
postgres-mcp |
Stored injection in a TEXT column the agent reads |
Adding your favourite server: see docs/INTEGRATIONS.md and the per-server template in tests/integration/.
Test coverage
pytest tests/ jumps from 121 cases (v0.2) to 203 cases (v0.3), all green. New surfaces:
tests/test_stats.py(21 cases) — windowing, percentiles, JSON shape.tests/test_telemetry.py(25 cases) — env handling, identity stability, payload privacy contract, transmission paths.tests/test_health.py(7 cases) — loopback-only enforcement, hostile-peer survival.tests/test_lint.py(14 cases) — basic + strict mode, drifted attack-examples, malicious YAML.tests/integration/{github,brave_search,postgres}/*.py(9 cases) — smoke + benign + attack per server.
Compatibility
- Python ≥ 3.11.
- AGPL-3.0-or-later.
- No new runtime dependencies. All four new modules (
stats.py,telemetry.py,health.py,lint.py) reuse existing deps. - Detector is off by default. Telemetry is off by default and opt-in only. Health endpoint is off by default. Existing v0.2 users keep their behaviour.
Migrating from v0.2.0
pip install -U mcp-firewall(orpip install -e .from the repo).- Existing
log.dbopens unchanged — schema version stays at 2. - To get the new audit-fix protections, no config change is needed; they are always-on improvements to the detection layer.
- To enable observability:
mcp-firewall stats # local read of the audit log mcp-firewall run ... --health-port 8765 # k8s/docker probe export MCP_FIREWALL_TELEMETRY=true # opt-in telemetry, off by default
Known limitations (v0.4 backlog)
- ReDoS guard for community-contributed regex (no pattern-time budget yet).
- Per-id error synthesis on JSON-RPC batch block (the wire currently desyncs from the audit log when only some members block).
- Health endpoint per-connection timeout + read-byte cap.
- Cross-script homoglyph coverage (Cyrillic look-alikes for Latin letters) — needs a
confusablestable (~10 MB). - Anthropic Haiku fallback tier (carried over from v0.2).
Tracking is in docs/AUDIT-REPORT-week3.md §"Deferred to v0.4".
Acknowledgements
The Week-3 adversarial review (10 findings, 4 closed here, 6 documented for v0.4) was performed by a sub-agent run against the codebase. Same threat model as ADR-0004 / ADR-0005.
Rule-pack signatures continue to be sourced from public corpora — see docs/THREATS.md for the per-rule provenance table.
v0.2.0 — Detection layer (rules + LLM classifier)
mcp-firewall v0.2.0 — Detection Layer
Date: 2026-05-04
Status: alpha — opt-in detector; Week 1 audit-only behaviour preserved when detector is off.
Headline
mcp-firewall now enforces, not just observes. Turn on the detector and every JSON-RPC frame goes through a rules + local-LLM cascade, with high-confidence prompt-injection payloads in tool results replaced by a sanitised isError: true reply before the agent ever sees them. The original bytes stay in the audit log for forensics. Off by default, so existing Week 1 users keep their latency profile until they opt in.
ollama pull qwen2.5:3b # optional — rules-only mode works without it
mcp-firewall run --server "..." --detector
mcp-firewall logs --verdict BLOCK --tail 50What's new
The detection cascade (ADR-0004)
RulesEngine— 24+ regex signatures shipped as YAML packs. Catalogued indocs/THREATS.mdwith source URLs (garak, promptfoo, Trojan Source, embracethered, MITRE ATT&CK).OllamaClassifier— local LLM verdict viaqwen2.5:3b. SHA-256 cache, circuit breaker (3 failures → 60 s open), hard 1 s per-request timeout.Policy— YAML-driven first-match rule engine. Default actionallow; built-in rules block on score ≥ 0.85 and warn on bare classifier signal. Custom policies welcome — drop a YAML and pass--policies <path>.Inspector— orchestrator. Hard latency abort at 1.25×max_latency_msfalls back toWARNso a slow Ollama can never wedge the pump.
CLI
| Command | What it does |
|---|---|
mcp-firewall run --detector |
Run the proxy with the detection layer on. |
mcp-firewall run --policies <path> |
Override the built-in policy. |
mcp-firewall detect "<text>" |
Run the cascade over a single string. Exit 0 = PASS, 1 otherwise. |
mcp-firewall logs --verdict BLOCK |
Filter the audit log to blocked frames. |
Storage
Schema bumped from v1 to v2:
eventsgains sixdet_*columns (verdict, score, rules, classifier, latency, action).- New
classifier_cachetable for the SHA-256 verdict cache. - Partial-failure-safe migration via
BEGIN IMMEDIATEand idempotentALTER TABLE.
A Week-1 log.db opens cleanly under v0.2.0 — the migration runs once on first open and writes a new schema_version=2 row alongside the existing 1.
Performance budget
ADR-0004 §7 numbers, validated by tests/test_perf.py and against a real Ollama:
| Path | Budget | Measured (M-series Mac) |
|---|---|---|
| Rules detector | ≤5 ms p95 | 0.04 ms p95 |
| Inspector cache hit | ≤10 ms p95 | 0.13 ms p95 |
| Inspector with LLM (cache miss) | ≤200 ms p95 | ~146 ms p50, ≤163 ms p95 |
| Hard inspector abort threshold | 250 ms | enforced in code |
Cold Ollama call (~1.8 s on first model load) busts the p95 budget — handled by the hard-abort path: that one frame returns WARN instead of stalling the pump, then subsequent frames stay under budget. Warm-up tip in docs/RUNBOOK.md.
Test coverage
pytest tests/ jumps from 27 cases (Week 1) to 121 cases (Week 2), all green. Highlights:
test_proxy_block.pyruns the real CLI as a subprocess, feeds it a prompt-injection tool result, and asserts the agent receives the sanitised replacement (not the injection).test_storage_migration.pyexercises a hand-rolled v1 DB through the live migration, including a "crashed mid-migration" recovery scenario.test_detectors_rules.pyparametrises 30+ canonical PoCs from public sources and asserts each is caught by the right rule id.test_perf.pyasserts the latency budgets above with real numbers.
Self-audit findings (5 fixed in this release, 5 deferred)
Full report in docs/AUDIT-REPORT-week2.md. Headline:
Fixed:
- Classifier prompt-injection via
>>>/Answer:tokens — content is now sanitised before substitution. - Unknown
when:keys in policies — now rejected at load time (a typo would otherwise silently match every frame). - Synthetic-block s2c row missed
det_verdict— now propagated from the parent c2s decision. _trace_idwas predictable — now usesos.urandom.- Migration used
BEGIN(deferred) instead ofBEGIN IMMEDIATE— now properly serialised.
Deferred to v0.3 (documented in docs/THREATS.md §"Limitations"):
- LLM cascade only inspects
result.content[*].textblocks; non-text shapes bypass the classifier. - No NFKC normalisation in rules — homoglyph / per-word zero-width attacks slip through some patterns.
- Batch JSON-RPC frames inherit a single inspection verdict across all members.
- Anthropic Haiku as a third-tier fallback — Ollama-only or rules-only today.
- Truncation seam in
_truncatecan split a marker across the discard boundary.
Compatibility
- Python ≥ 3.11.
- AGPL-3.0-or-later.
- No new runtime dependencies.
httpxwas added in Week 1 in anticipation of this milestone. - Detector is off by default. Set
detector.enabled: truein config or pass--detectorto opt in.
Migrating from v0.1.0
pip install -U mcp-firewall(orpip install -e .from the repo).- Existing
log.dbmigrates automatically on first open. To verify:sqlite3 ~/path/to/log.db 'SELECT MAX(version) FROM schema_version;' # → 2
- To turn the detector on: add
detector.enabled: trueto your config, or pass--detectoronce on the command line. - Optional: pull
qwen2.5:3bvia Ollama for the LLM-classifier path. The detector works in rules-only mode without it.
Acknowledgements
Rule signatures sourced from:
- garak (probes/promptinject, probes/exploitation)
- promptfoo (templates/redteam)
- Trojan Source (CVE-2021-42574)
- embracethered.com (markdown-image exfil, Unicode tags, conditional injection)
- Simon Willison's prompt-injection tag
- MITRE ATT&CK (T1027.013, T1059.004, T1070.003)
- GTFOBins (reverse-shell incantations)
- jailbreakchat.com (DAN/AIM/STAN family)
Without these public catalogues this release would not exist.
v0.1.0 - stdio proxy + audit log
First working version: a transparent stdio proxy that sits between an MCP client (like Claude Desktop) and an MCP server, logs every JSON-RPC frame to SQLite, and lets you tail the audit log live.
What works
- stdio proxy with asyncio + subprocess, half-close handling, signal-aware shutdown
- Audit log in SQLite (WAL mode), batched async writes, drop-on-overflow protection
- CLI:
mcp-firewall run(proxy) andmcp-firewall logs --tail/--follow(viewer) - JSON-RPC 2.0 parser with pydantic v2 models, batch-aware
- Config precedence: CLI flags → env vars → YAML → defaults
Verified end-to-end
Tested against the real @modelcontextprotocol/server-filesystem@2025.11.25:
handshake → tools/list → list_directory → read_text_file all flow through cleanly, every frame captured in the audit log.
Quality gates
- 27 tests (models, storage, e2e proxy round-trip)
- ruff + ruff format + mypy strict — clean
- bandit + pip-audit — 0 issues
- All 10 commits validated against Conventional Commits
- CI matrix: Python 3.11 + 3.12
What's NOT in this release
No detection layer yet — that's Week 2 (rules-based regex + local LLM classifier via Ollama).
This release is just the plumbing: the proxy and the audit trail. You can run it now and see what your AI agent is actually doing through MCP, but it won't block anything.
Install
git clone https://github.com/churik5/mcp-firewall.git
cd mcp-firewall
pip install -e .
mcp-firewall run --server "npx -y @modelcontextprotocol/server-filesystem@2025.11.25 /tmp"See docs/RUNBOOK.md for full setup instructions and Claude Desktop integration.