mcpfz-probe
Standalone runtime probe for MCP server fuzzing. It runs beside
mcp-server-fuzzer: the
fuzzer owns process launch and per-call timing; this repo owns runtime event
collection, per-call attribution, and the sidecar protocol. The privileged
sidecar is Rust (small, auditable, static Linux artifact); the integration layer
is Python (a simple RuntimeMonitor). See the fuzzer's
Runtime monitoring
section for how the two fit together.
Install
The Python monitor + policy engine (used by the fuzzer):
pip install mcpfz-probe
CI also publishes a prebuilt, eBPF-enabled Linux sidecar binary on each v*
tag. Fetch the latest and point the fuzzer at it via MCPFZ_PROBE_BIN:
curl -L -o mcpfz-probe \
https://github.com/Agent-Hellboy/mcpfz-probe/releases/latest/download/mcpfz-probe-x86_64-linux-ebpf
chmod +x mcpfz-probe
Repo layout
crates/mcpfz-probe/ Rust sidecar binary (fake + eBPF backends)
crates/mcpfz-probe-ebpf/ BPF program (compiled to bytecode, Linux only)
crates/mcpfz-probe-ebpf-common/ #[repr(C)] event type shared kernel<->userspace
src/mcpfz_probe/ Python monitor + policy engine
tests/ CI-friendly Python tests (fake backend, no root)
examples/ Sample events + live eBPF demo
docs/ Architecture and protocol notes
Status
- Python monitor + policy engine: complete and tested.
fakebackend: complete. A deterministic test double that replays scripted events into the correct bucket (startup/call/ambient) attributed to the activecall_id— the whole pipeline runs on any OS without root.ebpfbackend: real, multi-probe. Loads a CO-RE BPF program onsyscalls:sys_enter_*tracepoints — exec (execve), process creation (fork/vfork/clone), network egress (connect,sendto), listener exposure (bind,listen), file open (openat), file mutation (mkdir,unlink,rmdir,rename,symlink,link), chmod (chmod/fchmodat), andptrace— attributes captured events to the active call window, and emits the same NDJSON as the fake backend. Verified on Ubuntu 24.04 / kernel 6.8 driven bymcp-server-fuzzer: a vulnerable server produced exec, TCP+UDP connect, sensitive read, out-of-workspace write, chmod, delete, and ptrace findings, each attributed to the exact tool call.
Sidecar protocol (NDJSON)
Python → sidecar stdin (control):
{"op":"scope","pgid":1234,"generation":1}
{"op":"mark","phase":"begin","call_id":"...","tool":"get_weather"}
{"op":"mark","phase":"end","call_id":"..."}
Sidecar → Python stdout (events); bucket is startup/call/ambient:
{"type":"exec","bucket":"call","call_id":"...","pid":1234,"argv":["/bin/sh","-c","curl ..."]}
{"type":"connect","bucket":"ambient","pid":1234,"dst":"203.0.113.7:443"}
Full spec in docs/protocol.md.
Using it (Python)
monitor.set_scope_pgid(server_pgid, generation=1)
monitor.begin_call(call_id, tool_name)
try:
result = call_tool(...)
finally:
summary = monitor.end_call(call_id) # events attributed to this call
findings = evaluate_events(summary.events, policy)
RuntimePolicy.net_allow accepts exact destinations (host:port), host names,
IP literals, and shell-style host globs. For example, api.example.com allows
api.example.com:443, while *.internal allows db.internal:5432. Exact
destination entries continue to work for port-specific policies.
CLI
mcpfz-probe --backend fake --events-file examples/events.sample.json
mcpfz-probe --backend ebpf # Linux, needs root/CAP_BPF
mcpfz-probe --help
The fake backend reads an events script ({"events":[...]} or a bare [...]),
each entry a trigger (startup/scope/begin/end, optional tool filter)
and an event object the sidecar passes through, filling in bucket, call_id,
and ts_ns. See examples/events.sample.json.
mcp-server-fuzzer integration
The probe plugs into mcp-server-fuzzer through three small, opt-in hooks
(module mcp_fuzzer/runtime_probe.py): scope the sidecar to the server's process
group when the stdio server launches, begin/end marks around each
_execute_tool_call, and merge the resulting findings into the session at the
end. When MCP_FUZZER_RUNTIME_PROBE is unset the hooks are no-ops.
export MCP_FUZZER_RUNTIME_PROBE=1
export MCPFZ_PROBE_BIN=/path/to/mcpfz-probe # the sidecar binary
export MCPFZ_PROBE_BACKEND=ebpf # or "fake"
sudo -E mcp-fuzzer --mode tools --protocol stdio \
--endpoint "python examples/vulnerable_server.py" \
--runs 3 --max-concurrency 1
Kernel-observed syscalls become findings attributed to the exact tool + run:
runtime.exec, runtime.process_spawn, runtime.net_connect,
runtime.net_bind, runtime.net_listen, runtime.sensitive_read,
runtime.fs_write, runtime.fs_delete, runtime.fs_chmod,
runtime.fs_mkdir, runtime.fs_rename, runtime.fs_symlink,
runtime.fs_link, and runtime.ptrace. Verified on Linux against
examples/vulnerable_server.py
(tools that shell out, read ~/.ssh, beacon over TCP/UDP, drop+chmod+delete a
file, and call ptrace): all seven categories were captured and merged into the
fuzzer's report, each attributed to the call that caused it.
Per-call attribution assumes stdio calls run serialized (--max-concurrency 1),
matching the design in docs/architecture.md; execs from overlapping calls still
get caught, but land in the ambient bucket.
Development
# Python tests
PYTHONPATH=src python3 -m unittest discover -s tests
# Rust sidecar (portable, fake backend)
cargo build
cargo test
eBPF backend (Linux)
Needs a BTF-enabled kernel and this toolchain:
rustup toolchain install nightly --component rust-src
cargo install bpf-linker # needs LLVM dev libs (e.g. llvm-18-dev)
cargo build --features ebpf # build.rs compiles the BPF crate to bytecode
sudo ./target/debug/mcpfz-probe --backend ebpf
Live end-to-end demo (real kernel capture + real policy), run as root:
python3 examples/ebpf_live_demo.py
Known limitation: scope filtering resolves a process's group via
/proc/<pid>/stat in userspace, which races short-lived processes. Per
docs/architecture.md, this should move into the kernel program
(task->group_leader via CO-RE) — next step alongside the connect/file_open
probes.
Metadata
Release files for mcpfz-probe 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mcpfz_probe-0.1.3.tar.gz | 14.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcpfz_probe-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 24.6 kB
Release files / mcpfz_probe-0.1.3.tar.gz
| Download URL | mcpfz_probe-0.1.3.tar.gz |
|---|---|
| Size | 14.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
086b46d5a703325031f575173d4d3a3a3d38a1846ef0ce6023cfd0b842fb251c
|
|
BLAKE2b-256 checksum How to use checksums |
716cbae6a0eb385d42eda156fa91c9e9a5c480e8895c6c80b07646e8332dd19b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / mcpfz_probe-0.1.3-py3-none-any.whl
| Download URL | mcpfz_probe-0.1.3-py3-none-any.whl |
|---|---|
| Size | 9.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
51bd542d9c8654eb2ce9ff53e701758d9aedafe73bc732bc80891cc9ca148650
|
|
BLAKE2b-256 checksum How to use checksums |
b1d71174370fd96e365aac622e8b06df78d5bed06acfdd8248a08f702eec5e41
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|