legion-guard — verify before connect
An agent reads an MCP server's tool descriptions as instructions. The server you approved last week can expose other
tools, or the same tools with other descriptions, today. legion-guard checks the tools/list your client actually
receives, at connect time.
import legion_guard
session = legion_guard.wrap(session, "com.dexpaprika/dexpaprika") # after session.initialize(); one line, no config file
tools = await session.list_tools()
print(session.report["verdict"]) # NO_CHANGE_DETECTED | CHANGE_DETECTED | UNVERIFIED
Any MCP client, no code: the stdio proxy
Claude Code, Cursor, Claude Desktop, Codex CLI, Gemini CLI: any client that launches a stdio server can run it through the guard. Replace the server command with:
python3 -m legion_guard proxy [--server-id io.example/server] [--mode warn|block] -- npx -y @scope/server --flag
or let it rewrite your config for review (it prints, it never writes):
python -m legion_guard wrap-config ~/.cursor/mcp.json > mcp.guarded.json
The proxy relays JSON-RPC unchanged and inspects every tools/list answer:
- tool fingerprints (name, description, inputSchema) against your first-use pin, the list seen at the start of the
session, and, with
--server-id, the list signed by the LEGION log; - tool annotations (
readOnlyHint,destructiveHint,idempotentHint,openWorldHint) against their first-seen values. Fingerprints do not cover annotations, but a client may rely on them to decide what runs without asking. A change toward less caution (read-only false→true, destructive true→false, open-world true→false) is reported as a change even when the fingerprint is identical.
warn (default) logs to stderr and ~/.cache/legion-guard/proxy-audit.jsonl and changes nothing. block removes changed,
new or relaxed tools from tools/list and answers their tools/call with JSON-RPC error -32001 without reaching the
server. Guard errors are logged and traffic passes unchanged. Tested end to end with the official MCP Python SDK client
(tests/test_proxy.py); stdio only (HTTP servers are left untouched by wrap-config).
Try it in about a minute (including the virtualenv + install)
python3 -m venv v && . v/bin/activate && pip install legion-guard
python -m legion_guard check https://mcp.dexpaprika.com/streamable-http com.dexpaprika/dexpaprika
It connects to the public DexPaprika MCP server (initialize + tools/list, no tool call), fingerprints the 20 tools it
receives and checks them against the latest list signed by the LEGION log. Look for "state": "MATCHES_SIGNED" (or
DIFFERS_FROM_SIGNED, UNKNOWN, UNVERIFIED); exit code 0 = no difference, 1 = difference, 2 = connection error.
Low risk to add (tested)
- Observes by default. The default mode is
warn: changes are logged (logging.getLogger("legion_guard")) and reported insession.report; no tool is hidden and no call is refused. - Never breaks your app. If a source is unreachable or fails, it is reported
UNAVAILABLEand nothing is blocked, in every mode. Internal errors of the guard are logged, never raised. - Blocking is opt-in.
wrap(session, server_id, mode="block")hides fromlist_tools()and refuses incall_tool()only the tools for which a source has evidence of a change (changed or new tool).UNKNOWNandUNAVAILABLEnever block.
Removable: LEGION is the default, not a lock-in
Sources are pluggable. Each one compares the received list with its own reference:
| Source | Reference | Network |
|---|---|---|
FirstUseSource() (default) |
the first list seen for this server, pinned in ~/.cache/legion-guard/pins.json |
none |
LegionSource() (default when a registry id is given) |
latest list signed by the LEGION transparency log: record + RFC 9162 inclusion proof + signed head, keys from a pinned root key | tlog.legion-api.com |
LegionSource(base=..., root_pin=...) |
the same check against another log that serves this API (for example a self-hosted mirror) | your log |
LegionSource(witnesses=[vkey, ...], min_witnesses=N) |
additionally requires the signed head to be a C2SP checkpoint signed by the log and cosigned by N of your witnesses | as above |
PinnedSource(dict_or_lockfile) |
fingerprints you pinned yourself (mcp-tools-lock/0.1, same format as legion-lock) |
none |
your own object with .name and .check(server_id, fp) |
anything | yours |
session = legion_guard.wrap(session, "io.example/server", sources=[legion_guard.PinnedSource(".mcp-tools.lock.json")]) # no LEGION at all
Measured on 2026-10-17: the 20-tool list served by mcp.dexpaprika.com/streamable-http matches the list LEGION signed
(MATCHES), also when the head must be a checkpoint cosigned by a chosen witness. There is no independent witness
of the LEGION log yet: the only cosigning witness today is run by LEGION itself.
Limits
- LEGION observes only part of the servers; absence from the log is not a verdict (
UNKNOWN). - A change is not an attack. This does not certify that a server is secure, and says nothing about what the tools do.
- First-use pinning trusts the first list it sees.
- With
witnesses=, the result isUNAVAILABLE(never blocking) until your witnesses have cosigned the log's current checkpoint. The only witness today is run by LEGION and cosigns hourly, so after each new head there is a window of up to about one hour where this option cannot confirm anything (measured on 2026-10-18). - Fingerprints cover
name,descriptionandinputSchema(JCS RFC 8785), as in the LEGION log. - Tested with the MCP Python SDK
ClientSession; frameworks that build on it are not tested yet.
The 0.1 functions (guard_session, evaluate, approve, python3 -m legion_guard check|approve) are unchanged.
Release notes
- 0.2.6 (2026-10-24):
python -m legion_guard proxy -- CMDruns any stdio MCP server through the guard, for any client that launches stdio servers;python -m legion_guard wrap-config FILEprints a config with its stdio servers wrapped (writes nothing). New signal: tool annotations compared with their first-seen values; a change toward less caution is reported (and blocked inblockmode) even when the fingerprint is identical. Tested with the MCP Python SDK client (mcp 2.1.1,tests/test_proxy.py17/17) and against@modelcontextprotocol/server-everything(13 annotated tools, identicaltools/listwith and without the proxy). The library API and thecheck/approveCLI are unchanged. - 0.2.5 (2026-10-21): requires
mcp>=1.24(wasmcp>=1.0). 1.24.0 is the first MCP Python SDK version withmcp.client.streamable_http.streamable_http_client, which the CLI uses. With 1.0–1.23.3 the package installed butpython3 -m legion_guard checkfailed with an ImportError (measured on 2026-10-21; 0.2.1–0.2.4 declaredmcp>=1.0). Tested on mcp 1.24.0, 1.30.0, 2.0.0, 2.1.1 and 2.3.0. The library itself does not importmcp. GHSA-84m7-p3x7-pcfv concerns MCP servers (fixed in 1.30.0 and 2.2.0); if the same environment also runs servers, use one of those versions. The test servers run on both the 1.x and the 2.x SDK. - 0.2.4 (2026-10-21): the vendored mirror verifier accepts the checkpoint note signed under the log's origin name (C2SP) as well as the historical name, same key.
Metadata
Release files for legion-guard 0.2.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| legion_guard-0.2.6.tar.gz | 58.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| legion_guard-0.2.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 111.4 kB
Release files / legion_guard-0.2.6.tar.gz
| Download URL | legion_guard-0.2.6.tar.gz |
|---|---|
| Size | 58.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ecdbc6ae062e58b5be8cb1ea8cddd17144fd46d0b73e2a853e1ba7486fa874ba
|
|
BLAKE2b-256 checksum How to use checksums |
2a12f0885d2287a6624e262fcba61fd1ddafaaee15be47c3fc0241437c73d1de
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / legion_guard-0.2.6-py3-none-any.whl
| Download URL | legion_guard-0.2.6-py3-none-any.whl |
|---|---|
| Size | 53.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5eba5e639955f35095fb690dc7782315e276c228281c7b994dd6572863352931
|
|
BLAKE2b-256 checksum How to use checksums |
6cab4f4135b299277794644a29cf5c4b80670f6815f08f3dbe596b480c1bdb16
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Security for legion-guard 0.2.6
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.