Skip to main content
Glama

scapy-mcp

Code style: crackerjack Runtime: oneiric Framework: FastMCP uv Python: 3.14+

MCP server wrapping scapy for packet crafting, dissection, pcap staging, and live BPF capture. Tools operate on a worker that holds a scapy_mcp.config.settings.ScapySettings instance and respect a four-stage transmit policy before any frame leaves the host.

Version: 0.1.1 — BSD-3-Clause — Python ≥ 3.14 — alpha status

Install

uv pip install scapy-mcp
scapy-mcp

The console script scapy-mcp resolves to scapy_mcp.__main__:main, which builds the FastMCP ASGI app and serves it on uvicorn (so the custom /readyz route is mounted). Set SCAPY_MCP_HTTP_PORT to override the default port 3056.

For local development:

uv sync --group dev
uv run scapy-mcp

Configure

Settings layer: defaults → settings/scapy-mcp.yamlsettings/local.yamlSCAPY_MCP_* environment variables (pydantic-settings nested delimiter is __). Nested config (e.g. auth) uses SCAPY_MCP_AUTH__ENABLED=true.

Setting

Default

Purpose

http_port

3056

MCP HTTP listener port

tool_profile

full

MINIMAL, STANDARD, or FULL profile

log_level

INFO

oneiric LoggingConfig level

default_iface

None

capture iface when caller omits one

capture_packet_cap

10000

per-session packet ceiling

capture_duration_cap_seconds

60

per-session wall-clock ceiling

capture_default_bpf_filter

None

global BPF filter applied when caller omits one

capture_executor_workers

2

ThreadPoolExecutor size for the sync sniff fallback

pcap_write_dir

<tmpdir>/scapy_mcp/pcap-staging

write-side staging root (path-containment enforced)

pcap_read_max_bytes

52428800

read-side ceiling; read_pcap refuses larger files before parsing

transmit_enabled

false

master kill-switch — must be true to emit any frame

transmit_allow_l3_cidrs

[]

L3 CIDR allow-list

transmit_allow_l2

false

allow L2 (ARP / ND / RAW) emission

transmit_allow_broadcast

false

allow broadcast destination

transmit_max_pps

10

per-session packets-per-second cap

transmit_max_per_session

100

per-session total packet cap

transmit_max_probe_targets

16

per-call probe-target count cap

Tools

The server registers 9 domain tools + the mcp-common baseline tools.

Group

Tool

Purpose

craft

craft_packet

Build a Packet from a LayerSpec discriminated union

dissect

dissect_bytes

Parse raw bytes into layers + summary text

pcap

read_pcap

Read packet summaries from a pcap at path, with offset/limit windowing

pcap

write_pcap

Stage a pcap into pcap_write_dir (paths outside the dir refused)

capture

capture_start

Start a BPF-filtered capture (optional feed)

capture

capture_stop

Stop an in-progress capture session

capture

capture_read

Read summaries from an active capture session

transmit

transmit_packet

Send a single packet after every transmit control passes

transmit

probe_packet

One-shot emit at a small BPF, used to confirm the surface works

health

health

Per-feed aggregate status (counts + last error)

Plus the four mcp-common baseline tools — discover_tools, get_liveness, get_readiness, health_check_all — registered unconditionally via bootstrap_baseline_tools.

read_pcap contract

read_pcap accepts an arbitrary path (a file produced by write_pcap, a pre-existing capture, or a fixture) and returns summaries. Before parsing, the file size is checked against pcap_read_max_bytes; oversize files are refused with ConfigurationError so multi-GB captures cannot exhaust memory by being loaded whole. Path containment under pcap_write_dir is the write path's concern, not read's.

Health

Two routes, answering different questions:

  • /health — always HTTP 200. Reports per-feed detail in components. For orchestrators and curl smoke probes.

  • /readyz — HTTP 503 when a required feed has not yet returned data, 200 otherwise. For readiness probes.

The capture feed is optional: its absence never flips /readyz to 503. All other registered feeds are required.

Four transmit controls

Every transmit_packet call is checked against:

  1. Master kill-switch (transmit_enabled: bool, default false). The default refuses every frame — set SCAPY_MCP_TRANSMIT_ENABLED=true to permit emission at all.

  2. L3 CIDR allow-list (transmit_allow_l3_cidrs: list[str]). Set to ["0.0.0.0/0"] for unrestricted L3; production deployments pin the specific CIDRs the worker is allowed to reach.

  3. L2 destination allow-flag (transmit_allow_l2: bool, default false). Pure ARP / ND / RAW frames require this flag; L3 packets do not.

  4. Broadcast opt-in (transmit_allow_broadcast: bool, default false). Even after the L2/L3 allow-list, broadcasts refuse unless this is true.

A refusal emits an EmissionRefusedError with the failing control name and a human-readable reason. The wrapper logs a scapy-write-would-refuse / scapy-transmit-refused warning so the refusal is visible without polluting the caller's error stream.

BPF probe (capture)

Capture is OPTIONAL. When /dev/bpf* is missing capture_start refuses with CapabilityUnavailableError and the capture feed is marked unavailable. /readyz stays 200 because capture is not a required feed.

Backend selection

The capture worker picks one of two backends at session start:

  • AsyncSniffer — preferred when scapy exposes it (≥ 2.5). Each session owns a sniffer; capture_stop calls .stop() and capture_read reads .results.

  • sniff(stop_filter=...) — synchronous fallback executed in the module-level worker pool. stop_filter checks a per-session cancel event so capture_stop works; packet_cap and duration_cap are enforced via count and timeout on the underlying sniff call.

If scapy exposes neither backend, capture_start refuses with CapabilityUnavailableError (reason = "no sniffer backend").

Deterministic fixtures

Tests do not open raw sockets. All packet construction is exercised against in-memory fixtures under tests/fixtures/. Each fixture includes a hand-crafted bytes() body that round-trips through craft_packetrdpcap/wrpcapdissect_bytes. No real network frames in the suite.

Regenerate via:

python -m scripts.gen_pcap_fixtures

Default = closed-by-default transmit

The shipped defaults cannot emit a frame:

transmit_enabled = false
transmit_allow_l3_cidrs = []
transmit_allow_l2 = false
transmit_allow_broadcast = false

A worker installs only what its operator explicitly approves. See scapy_mcp/config/settings.py for the full settings surface.

License

BSD-3-Clause.