This guide is the practical command checklist for developing
crates/mcp_runtime and the Python integration around it.
Use it together with:
- README.md for runtime architecture, modes, and operator workflows
- STATUS.md for the current branch-local validation and benchmark snapshot
- TESTING-DESIGN.md for the session/auth isolation threat model
Changes in the Rust MCP runtime often affect more than the crate itself. A single feature can touch:
- the Rust runtime crate
- Python integration in
mcpgateway/ - Docker/compose startup wiring
- MCP end-to-end tests
- admin UI pages such as Overview and Version Info
- load-test and benchmark behavior
That means a complete development loop is usually layered:
- fast Rust-local checks
- broader Python/backend checks
- live compose-backed MCP validation
- UI validation when relevant
- benchmark and profiling work when performance-sensitive paths changed
Use the smallest set that matches your change.
| Change type | Minimum checks |
|---|---|
Pure Rust refactor in src/ or tests/ |
make -C crates/mcp_runtime fmt-check clippy-all test test-rmcp |
| Rust + Python integration change | Rust-local checks plus make doctest test htmlcov |
| MCP protocol, auth, session, or transport behavior | Rebuild stack and run make test-e2e; add make test-mcp-plugin-parity with PLUGINS_CONFIG_FILE=plugins/plugin_parity_config.yaml for live plugin parity, make test-mcp-access-matrix for detailed role/output verification, make test-mcp-session-isolation for Rust public path work, and make test-mcp-session-isolation-load for correctness-under-load changes |
| Overview / Version Info / templates / JS / CSS | make test-js-coverage lint-web bandit interrogate pylint, plus make test-ui-smoke and targeted Playwright tests |
| Packaging / release readiness | make verify |
| Performance-sensitive hot path | relevant tests plus benchmark and profiling targets |
For day-to-day Rust work, stay in the crate until the code shape is stable.
make -C crates/mcp_runtime fmt-check
make -C crates/mcp_runtime check
make -C crates/mcp_runtime clippy
make -C crates/mcp_runtime clippy-all
make -C crates/mcp_runtime test
make -C crates/mcp_runtime test-rmcpWhat these cover:
fmt-check: formatting driftcheck: compile/type issues without full test costclippy: default all-target lint passclippy-all: all-targets, all-features lint passtest: default Rust teststest-rmcp: upstream RMCP client feature coverage
Use these when:
- editing
src/lib.rs,src/config.rs,src/main.rs - changing request routing, auth/session logic, direct DB paths, or helpers
- touching the optional RMCP path
These are the standard root-level formatting and linting commands the repo expects before a serious validation pass.
make autoflake isort black pre-commit
make test-js-coverage lint-web bandit interrogate pylint verify
make doctest test htmlcovNotes:
make autoflake isort black pre-commit- use after broader edits, especially mixed Python/template/doc changes
make test-js-coverage lint-web bandit interrogate pylint verify- this is the wider hygiene gate for Python, docs, web assets, and package metadata
make doctest test htmlcov- this is the main broad backend confidence pass
If you add or edit Python files while working on the runtime integration, keep their standardized headers valid:
make check-headersImportant behavior:
make testis a broad Python test run against thetests/tree with selected ignores- it does not run Playwright
- it does not run performance/compliance suites
- it now ignores
tests/live_gateway/, so live-infrastructure tests do not pollute the default backend test run
make doctestruns doctests againstmcpgateway/make htmlcovbuilds an HTML coverage report from.coveragemake verifybuilds the Python package and runs metadata/manifest checks
For most MCP runtime PR work, this is the minimum serious root-level gate:
make doctest test htmlcov
make bandit interrogate pylintAdd these when you touched UI/templates/static files:
make test-js-coverage
make lint-web
make test-ui-smokeAdd this when the change is close to shipping:
make verifyThe Rust live-stream branch in forward_transport_request (src/lib.rs)
relays Python's GET /mcp SSE stream byte-for-byte after parsing it back
into typed Event frames. As of ADR-052, Python serves a spec-conformant
SSE stream backed by a per-session event bus (Redis Pub/Sub + ring buffer in
multi-node, in-process deque + asyncio.Event in single-node). The relay
itself did not need code changes — it inherited the new behavior the moment
Python stopped returning 405.
What this means for development:
- The
session_id.is_none()405 in the live-stream branch is spec-mandated (GET requires a session id) and stays. Tests that assert on this 405 are still valid. - For the GET-with-session path, the relay opens an upstream GET to Python's
/mcp, parses the resulting SSE byte stream, and re-emits each frame. No Rust event-bus machinery needed; Python is the source of truth. - The Pub/Sub channel (
mcp:session:{sid}:events) and event-store keys (mcpgw:eventstore:{sid}:*) are owned by Python. Rust does not write to them in v1. A future iteration could have Rust subscribe directly to skip the Python hop, but the current relay is the simpler design and keeps the fanout policy in one place. live_stream_core_enabled()continues to gate this path; deployments that want Python to serve the stream end-to-end (no Rust relay) leave this disabled and let the request fall through to the Python proxy.
When debugging GET-stream failures, check Python first — make logs for
ServerEventBus lines reveals the chosen backend and any publish errors.
Rust-local tests are not enough for this runtime. You also need live validation against the compose-backed gateway.
Use this when you want to confirm the non-Rust public MCP path still behaves correctly.
make testing-down
make compose-clean
make docker-prod DOCKER_BUILD_ARGS="--no-cache"
make testing-up
make test-e2e
make test-mcp-access-matrix
PLUGINS_CONFIG_FILE=plugins/plugin_parity_config.yaml make testing-up
MCP_PLUGIN_PARITY_EXPECTED_RUNTIME=python make test-mcp-plugin-parityExpected outcome:
/healthreports Python MCP modemake test-e2epasses, with the Rust-only raw-header assertion skippedmake test-mcp-access-matrixpasses and verifies scoped-user access with strong tool/resource/prompt sentinelsmake test-mcp-plugin-paritypasses with the Python runtime header and proves activeresource_post_fetch,tool_post_invoke, andprompt_post_fetchbehavior on the public MCP path
Use this when validating that Rust can be present without owning the public MCP path.
make testing-rebuild-rust-shadow
make test-e2e
make test-mcp-access-matrixExpected outcome:
- public
/mcpstill behaves like Python mode /healthshows Rust present internally but Python mounted publicly
Use this when validating the Rust public transport edge without the full Rust session/event-store stack.
make testing-rebuild-rust
make test-e2e
make test-mcp-access-matrixUse this when validating the fullest Rust path and any session/replay/live stream/auth reuse changes.
make testing-rebuild-rust-full
make test-e2e
make test-mcp-access-matrix
make test-mcp-session-isolation
make test-mcp-session-isolation-load MCP_ISOLATION_LOAD_RUN_TIME=30s
cargo test --release --manifest-path crates/mcp_runtime/Cargo.toml
PLUGINS_CONFIG_FILE=plugins/plugin_parity_config.yaml make testing-rebuild-rust-full
MCP_PLUGIN_PARITY_EXPECTED_RUNTIME=rust make test-mcp-plugin-parityExpected outcome:
/healthreports Rust-managed runtime and Rust-mounted public transportmake test-e2epassesmake test-mcp-access-matrixpasses on the Rust pathmake test-mcp-session-isolationpasses on the Rust pathmake test-mcp-session-isolation-loadvalidates owner traffic and hijack denial under concurrent Locust loadmake test-mcp-plugin-paritypasses with the Rust runtime header and proves the live plugin hooks still affect public MCPresources/read,tools/call, andprompts/get- release Rust tests pass
For revocation and membership/role-drift changes, validate with a short reuse TTL so the bounded-TTL contract completes quickly:
MCP_RUST_SESSION_AUTH_REUSE_TTL_SECONDS=2 MCP_RUST_SESSION_AUTH_REUSE_GRACE_SECONDS=1 make testing-rebuild-rust-full
make test-mcp-session-isolation
make test-mcp-session-isolation-load MCP_ISOLATION_LOAD_RUN_TIME=30s
make test-mcp-access-matrixDo not assume the stack is in the mode you intended. Check it.
curl -sD - http://localhost:8080/health -o /dev/null | rg 'x-contextforge-mcp-'What to look for:
- Python baseline:
x-contextforge-mcp-runtime-mode: pythonx-contextforge-mcp-transport-mounted: python
- Rust shadow:
x-contextforge-mcp-runtime-mode: rust-managedx-contextforge-mcp-transport-mounted: python
- Rust edge/full:
x-contextforge-mcp-runtime-mode: rust-managedx-contextforge-mcp-transport-mounted: rust
If you changed the admin UI runtime display, also verify:
- Overview page shows
🐍 Python MCP Coreor🦀 Rust MCP Core - Version Info page shows the MCP runtime card with the correct mounted/core modes
You do not need these for every pure Rust refactor. You do need them when the change touches:
mcpgateway/templates/mcpgateway/static/mcpgateway/admin.pymcpgateway/version.py- Overview / Version Info runtime display
Recommended UI/web checks:
make test-js-coverage
make lint-web
make test-ui-smoke
uv run pytest tests/playwright/test_version_page.py -qBroader UI pass:
make test-ui-headlessNote:
make test-ui-headlessexercises broad repo UI behavior and can expose unrelated flaky admin flows- use targeted Playwright files first when you only changed one page
For Python coverage:
make coverage
make htmlcov
make doctest-coverage
make diff-coverFor Rust coverage:
make rust-coverage
make rust-diff-coverCoverage guidance:
- use
make htmlcovfor a fast local report once.coveragealready exists - use
make coveragewhen you need to regenerate the full Python coverage set - use
make diff-coverwhen you need changed-line coverage against the main branch - use
make rust-coveragewhen you need workspace Rust coverage with terminal, HTML, and Cobertura XML reports - use
make rust-diff-coverwhen you need Rust changed-line coverage against the main branch - use runtime-local
make -C crates/mcp_runtime coverageonly when you are explicitly improving that crate in isolation
Benchmark from the repository root against a compose-backed testing stack.
Quick benchmarks:
make benchmark-mcp-mixed
make benchmark-mcp-toolsHigher-concurrency distributed benchmarks:
make benchmark-mcp-mixed-300
make benchmark-mcp-tools-300Useful overrides:
make benchmark-mcp-tools-300 MCP_BENCHMARK_HIGH_USERS=1000 MCP_BENCHMARK_HIGH_RUN_TIME=60s
make benchmark-mcp-tools-300 MCP_BENCHMARK_HIGH_USERS=1000 MCP_BENCHMARK_HIGH_RUN_TIME=300s
make benchmark-mcp-mixed-300 MCP_BENCHMARK_HIGH_USERS=300 MCP_BENCHMARK_HIGH_RUN_TIME=60sHow to read the results:
benchmark-mcp-tools*- cleanest signal for the Rust hot path
- use this when evaluating transport/runtime improvements
benchmark-mcp-mixed*- exercises broader seeded data and fixture behavior
- useful, but noisier than tools-only numbers
Expected outcomes:
- Rust
edge|fullshould materially outperform the pure Python path on the tools-only workload shadowshould behave like the Python public path, not like the Rust public path- compare current results against the latest snapshot in STATUS.md rather than treating one hardcoded number as a release threshold
For Rust-local profiling:
make -C crates/mcp_runtime setup-profiling
make -C crates/mcp_runtime flamegraph-test
make -C crates/mcp_runtime flamegraph-test-rmcpArtifacts are written under:
crates/mcp_runtime/profiles/
When to use profiling:
- after a benchmark regression
- after a change to direct
tools/call, session handling, event store, or RMCP client reuse - when you need proof that a suspected Rust hotspot is real
Interpretation guidance:
- one-shot flamegraphs are often setup-heavy
- steady-state compose benchmarks are still the primary signal for end-to-end throughput
- if a benchmark regresses but the crate-local flamegraph does not show a Rust hotspot, the issue may be in Python, upstream MCP servers, networking, Redis, or compose/container behavior
make -C crates/mcp_runtime fmt-check
make -C crates/mcp_runtime clippy-all
make -C crates/mcp_runtime test-rmcpmake -C crates/mcp_runtime fmt-check clippy-all test-rmcp
make doctest test htmlcov
make bandit interrogate pylint
make testing-rebuild-rust-full
make test-e2e
make test-mcp-access-matrix
make test-mcp-session-isolationmake autoflake isort black pre-commit
make test-js-coverage lint-web bandit interrogate pylint
make doctest test htmlcov
make test-ui-smoke
uv run pytest tests/playwright/test_version_page.py -qmake -C crates/mcp_runtime fmt-check clippy-all test-rmcp
make testing-rebuild-rust-full
make benchmark-mcp-tools-300 MCP_BENCHMARK_HIGH_USERS=1000 MCP_BENCHMARK_HIGH_RUN_TIME=60s
make benchmark-mcp-tools-300 MCP_BENCHMARK_HIGH_USERS=1000 MCP_BENCHMARK_HIGH_RUN_TIME=300s
make -C crates/mcp_runtime flamegraph-testmake autoflake isort black pre-commit
make test-js-coverage lint-web bandit interrogate pylint verify
make doctest test htmlcov
make testing-rebuild-rust-full
make test-mcp-protocol-e2e
make test-mcp-rbac
make test-mcp-access-matrix
make test-mcp-session-isolation
cargo test --release --manifest-path crates/mcp_runtime/Cargo.tomlIf the change affects fallback behavior or public MCP mounting:
make testing-up
make test-e2eBefore calling a Rust MCP runtime change ready, the following should be true for the scopes you touched:
- Rust-local lint/test passes
- repo-wide Python/backend lint/test passes
- live compose-backed MCP tests pass in the relevant runtime mode
- Python baseline still works if you changed shared transport or fallback logic
- benchmark results are not materially worse than the current branch snapshot in STATUS.md without a clear explanation
- UI pages render the correct runtime mode if you touched admin/status surfaces
If you are unsure which commands to run, default to the broader workflow:
make autoflake isort black pre-commit
make test-js-coverage lint-web bandit interrogate pylint verify
make doctest test htmlcov
make testing-rebuild-rust-full
make test-e2e
make test-mcp-access-matrix
make test-mcp-session-isolation
cargo test --release --manifest-path crates/mcp_runtime/Cargo.toml