Development
Condensed from CONTRIBUTING.md — the repo file is the authoritative
version.
Setup
Prerequisites: Python 3.10+ (3.11 is what CI and the image use), pip,
make, and docker (only for image targets).
git clone https://github.com/soulwhisper/mcp-guardrails.git
cd mcp-guardrails
make dev # pip install -e ".[dev]" — pytest, ruff, grpcio-tools
make proto # regenerate stubs (no-op if proto/ext_mcp.proto is unchanged)
make test # unit suite, ~2s
make lint # ruff check
The pure-Python policy core (models, aggregator, invariant engine, regex
scanner, servicer) runs without the ML stack. The dev install
deliberately does not pull in onnxruntime / transformers, and CI
hard-fails if either leaks back into the unit-test path — heavy imports must
stay lazy.
Test pyramid
- Unit suite (
make test, 320+ tests): aggregator fail-closed table; invariant engine (ordered subsequence matching, negate guards, LoopRule fingerprinting, RateLimitRule / AggregateRule time windows, dotted-path resolution); scanners (regex patterns, PromptGuard grey-zone thresholds, AgentAlignment pre-egress redaction, truncation,extract_texthidden Unicode preservation); engine (timeout/exception handling, second-stage gating, webhook notification); gRPC servicer (in-process round-trip + wire mapping); rule loader; graceful-shutdown ordering;guardrail_ctlCLI. - Property tests (
tests/test_property.py, hypothesis):scan_windows,extract_text,_safe_json_loads,redact_value(skips gracefully when hypothesis is absent). - Red-team capability baseline (
tests/test_redteam.py): base64 injection, zero-width/confusables, markdown-image exfil,### SYSTEMcase variants, head/mid/tail padding bypasses, window-flush sequences. Residual gaps arexfail(strict=False)with the residual documented. - E2E smoke (
python3 tests/e2e_smoke.py): boots a live server and exercises the ExtMcp gRPC surface. - Interop e2e (
scripts/e2e_agentgateway.sh): real agentgateway binary — see End-to-end test. - Field / load tests (
tests/field_test.py,tests/load_test.py): requireonnxruntime+ the model cache.
filterwarnings = ["error", ...] in the pytest config — new warnings fail
the suite; fix the warning rather than suppressing it.
Proto stub sync
proto/ext_mcp.proto is vendored from agentgateway/agentgateway. The
generated stubs (ext_mcp_pb2.py, ext_mcp_pb2_grpc.py) are committed
so CI and the image build do not need grpcio-tools. When you change the
proto, run make proto and commit all three files together — the CI
proto-check job regenerates and runs git diff --exit-code; drift fails
the build. Field numbers are the wire-compat surface; field names are
cosmetic.
Release process (release-please)
Releases are automated via Release Please, driven by conventional commits:
- Every push to
mainupdates a self-maintaining release PR (changelog - version bumps in
pyproject.toml,guardrails/__init__.py,CHANGELOG.md). - Merging the release PR creates the tag + GitHub Release; the tag push
triggers
docker-publish.yml(multi-arch image). - Commit prefixes map to semver:
fix:/perf:patch,feat:minor,feat!:major;docs:/ci:/test:/chore:/build:/refactor:produce no version bump. - Image tags:
x.y.zis immutable (never re-point);latestandx.yfloat. ARELEASE_PLEASE_TOKENPAT (classic,reposcope) is required for the tag push to trigger downstream workflows.
Supply chain & operator tooling
- Dependency audit —
make auditruns pip-audit againstrequirements.txt(the locked runtime surface). Accepted-risk CVEs are whitelisted one-per-line inscripts/pip-audit-ignore.txt(passed as--ignore-vuln); every entry needs a written justification. pip-audit is a dev-extra dependency, never a runtime one. - SBOM —
make sbom(orIMAGE=<image> bash scripts/gen_sbom.sh) generates SPDX + CycloneDX SBOMs with syft intosbom/. CI wiring (generate on release, attach as release assets, sign) is intentionally left to the workflow owner — it needs a token with release-asset scope; the script is the CI-ready entrypoint. - Operator CLI —
scripts/guardrail_ctl.py(rules lint,decision replay,audit verify) is documented in Auditing.
Adding things
- A scanner — implement the
Scannerprotocol (async scan(content, role, *, context) -> ScanResult), wrap sync ML inference inasyncio.to_thread, register it inGuardrailEngine.from_configbehind anenable_*flag with anImportErrorfallback, add an env knob inconfig.py, document it in the README/config tables, and cover allow/block/review + timeout/exception paths with tests. - An invariant rule — add to
guardrails/rules/default.py(ships everywhere) or a custom pack; see Invariant rules. Validate withguardrail_ctl rules lint.
PR checklist (short form)
make lintclean,make testgreen.python3 tests/e2e_smoke.pygreen if you touched servicer / engine / config / server entrypoint.make protore-run and stubs committed if you touched the proto.- New deny pattern / scanner / rule → a test asserting the deny fires.
- New env var → read in
config.pyand documented in the env table. - DCO signoff (
git commit -s) and conventional-commit messages. - Security-sensitive paths (
aggregator.py,engine.py,invariant.py,scanners.py,rules/, the proto) get extra review — call out the security impact in the PR description.