legion-lock — pin MCP server tools in a lockfile, fail CI on tool drift
An MCP server can change a tool's description or input schema after you approved it, and a model reads those
descriptions as instructions (tool drift, "rug pull"). legion-lock pins the approved tool list of each MCP server in
.mcp-tools.lock.json. check fails as soon as a server changed, and names the change: tool added, removed,
description changed, schema paths changed.
Fingerprints come from the public LEGION transparency log of MCP tool lists (https://tlog.legion-api.com/tlog/v1).
Each one is signed (Ed25519), included in an append-only Merkle tree (RFC 9162) and checked against a root key
fingerprint you pin. The lockfile keeps the proofs: verify re-checks them offline.
Pin in 3 commands
python3 legion_lock.py add io.guestgraph/mental-model # pin the current state (verified proof) in .mcp-tools.lock.json
python3 legion_lock.py check # 0 = matches · 1 = DRIFT (per-tool diff) · 2 = invalid proof
python3 legion_lock.py verify # offline: re-check the proofs stored in the lockfile
The id is the one of the official MCP registry. To approve an earlier version: add ID --at-leaf N. To prove it
against a specific signed head: --at-head S.
Requirements: Python 3.8+, no dependency (cryptography is used if installed, otherwise pure-Python Ed25519).
Files: legion_lock.py, plus vendor/legion_tlog_verify.py and vendor/legion_verify_mirror.py, LEGION's two public
verifiers.
Check one live server in one command: legion_verify_server.py
python3 legion_verify_server.py https://mcp.example.com/mcp --save proof.json # live tools/list vs the signed history
python3 legion_verify_server.py --offline proof.json # re-check the saved proof, no network
Verdict: UNCHANGED (live list = latest signed version) · CHANGED (per-tool diff) · UNKNOWN (never signed by the
log) · AUTH (server requires auth). It only sends initialize and tools/list, never calls a tool. No score.
Demo (real drift, public log)
demo/demo_drift.sh pins two servers to a state BEFORE a change they really made, then runs check:
io.guestgraph/mental-model (tool diagram: description and 2 schema paths changed, leaf 16044 → 17659) and
ai.daystruct.api/daystruct (tool plan_project_development added, leaf 121 → 14516). Expected: exit 1 with the drift
named; offline verify = 0; hand-edited lockfile = 2. Transcript: demo/transcript_drift_*.txt.
Adversarial tests (lying log, forged archive, rewritten history, forged lockfile): tests/test_adversarial.py, 15/15.
What check verifies, in order
- The log keyring starts from the pinned root fingerprint
a7c6a35b…. - Your pin and the current state are both signed records, included in signed heads (RFC 9162).
- The current head extends the head of your pin (consistency proof). Otherwise: rewritten history, fork or rollback, exit 2.
- Is the current fingerprint the approved one? If not,
checkfetches the archived definitions of both versions, accepts them only if they hash back to the signed fingerprints, then names the drift field by field.
Limits (read them)
- This is the signed observation of a single observer (LEGION). It does not show what the server serves to other clients.
- Only
name,descriptionandinputSchemaare fingerprinted. A change limited toannotations,outputSchemaortitleis not seen. - Delay: a change appears only after LEGION's next observation, then the next signed head (one to three signed cycles a
day, measured 9–11 Oct 2026).
check --strictalso fails when a newer observation is not signed yet. - Servers that require authentication are not observed.
- The log's Bitcoin anchors are
pending(noots verifyon a full node).legion-lockdoes not rely on them.
Integrations (integrations/)
github/mcp-tools-lock.yml: on every pull request and daily,verifythencheck. The job fails on drift.pre-commit/pre-commit.sh(raw git hook).claude-code/settings.json+legion_lock_hook.sh:PreToolUsehook onmcp__.*. Claude Code blocks the MCP tool call if a pinned server drifted. Result cached 10 min.
Lockfile format: FORMAT.md. Read-only MCP server on the same log: legion-mcp. License: MIT.
Metadata
Release files for legion-lock 0.1.1
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_lock-0.1.1.tar.gz | 43.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| legion_lock-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 87.8 kB
Release files / legion_lock-0.1.1.tar.gz
| Download URL | legion_lock-0.1.1.tar.gz |
|---|---|
| Size | 43.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f41ede0c10161ba581c0b7f1a1698e2134c09f165bd9bc6062aa2cc5780f6059
|
|
BLAKE2b-256 checksum How to use checksums |
5c34754c9f6845e0608cc7edba6baacddd7e1d597d40720f77eba79c693df496
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / legion_lock-0.1.1-py3-none-any.whl
| Download URL | legion_lock-0.1.1-py3-none-any.whl |
|---|---|
| Size | 44.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e51e7bf3be129e121c8df3e59378ab0e9cb684bf6ba1b511d25f13d45b8849a3
|
|
BLAKE2b-256 checksum How to use checksums |
c1f3b2c4ded93bb2d27e5008548e32f2be359fcf9941c94b2503b608f39cdc1e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Security for legion-lock 0.1.1
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.