The tool you
approved is not
the tool you run.
You add an MCP server, your client shows you the tool descriptions, they look fine, you click approve. That decision is never revisited. The server can serve one set of definitions on Monday and a different set on Tuesday, and no major client re-prompts. mcp-pin fingerprints what you approved and blocks the session when it moves. Code: mcp-pin. Public log: mcp-pin.gautamkhosla.com. MIT, zero runtime dependencies.
SEC. 01
A consent that is never re-asked
Approval in MCP is a single event at connect time. The client renders the tool list, you read it, you accept. Nothing in the protocol requires the server to serve you the same list again, and nothing in the clients I checked notices when it does not.
The reason this matters more than it sounds is the position tool descriptions occupy. They are not passive metadata sitting next to the model. They are text the model reads as guidance about what to do next, which puts a description edit in the same blast radius as a system prompt edit, delivered by a third party, after you stopped looking.
The silence is the vulnerability. A server that changes what its tools say, between the session where you consented and the session where it runs, produces no signal at all.
SEC. 02
Two surfaces, one engine, zero inference
The proxy sits in front of a server you already run. First connect pins the full metadata of every tool. Every connect after that re-derives the same fingerprint and compares. If anything moved, the session stops and you get a diff.
The ordering here is the part that took the longest to get right. Client traffic is queued until the check completes, and on drift nothing queued is forwarded. An earlier version completed the handshake first and reported afterwards, which is a notification system wearing a control system's name.
The tool never asks a model whether a change looks dangerous. It computes a hash and compares it. That is deliberate: a deterministic check keeps working when a model has a bad day, and it keeps working on the subtle changes a model would wave through.
SEC. 03
What counts as a change
The whole tool object is fingerprinted: name, description, input schema and annotations, canonicalized per RFC 8785 and hashed with SHA-256. Reformatting is invisible. A single character of a description is not.
Scoping this by hand would have been the obvious mistake. Any rule of the form "watch the description but not the schema" hands an attacker the exact field to use, and a schema change that adds an argument the model is told to populate is a description change wearing different clothes. The rule is mechanical instead: if the model can read it, it is in scope.
SEC. 04
A history for what you install
The proxy answers "did this change since I approved it". The log answers a question the proxy structurally cannot: what did a server you did not write look like last Tuesday. Nobody was keeping that record, so there was no way to check it retroactively.
The point of a transparency log is that you do not have to trust whoever runs it. Entries are hash linked, the head is signed with Ed25519, and the verifier pins the published key rather than trusting one that arrives alongside the data. Downloading the log and verifying it takes one command and does not involve me.
The badge server authors can display only ever states a fact about time. It says unchanged 91d or changed today. It never says safe, because the project cannot know that and will not imply it.
SEC. 05
The condition for stopping
One kill test, written on 1 September before any code existed. By 15 October 2026 either ten public MCP server READMEs carry the badge, or there are a hundred unique proxy installs excluding CI. If neither, the repository is archived and the numbers are published as they stand.
It measures adoption rather than correctness on purpose. I can already show the hash comparison works; that proves nothing about whether the problem was worth solving. The thing I do not know is whether anyone else considers unannounced tool drift a risk they will spend sixty seconds on, and only strangers can answer that.
It sits in the README, above the fold, rather than in a private note, because a stop condition nobody can see is a stop condition I can argue my way past.
SEC. 06
Someone got there first
mcp-warden exists, it is at v1, it uses the same RFC 8785 plus SHA-256 canonicalization, and on schema diffing it is better than this project: it classifies each mutation individually rather than reporting one opaque change, and it uploads SARIF to code scanning.
That comparison is in my README, recommending warden for the job it does better. Discovering it after I had built most of this was uncomfortable, and burying it would have been the cheap move, but a security tool that hides the competitor has already told you what kind of tool it is.
The overlap turned out to be narrower than it first looked. A lockfile tells you that your own server changed since your last commit. It cannot tell you what a third party's tools said last Tuesday, because nobody kept that record. That gap is the only thing this project justifies, and stating it plainly forced me to work out whether there was one.
SEC. 07
What it does not protect against
A local pin store cannot defend against a process running as you. That program can delete the store and re-pin itself. This is an architectural limit rather than a bug, and it will not be fixed by writing the same files harder.
Day-one malice is invisible for the same structural reason. The tool detects change; a server that ships hostile definitions on the first connect and never touches them again is, to a difference engine, perfectly well behaved.
One limitation cuts against the project's own reason to exist and is in the table anyway. Testing on 2 September showed Claude Desktop refusing obvious injected instructions inside tool descriptions and warning the user unprompted. The model catches the loud version already. The argument for a deterministic check is that it does not depend on the payload being obvious, and that is a narrower argument than the one I started with.
SEC. 08
What broke, and what I would change
Versions at or below 0.1.0 silently lost concurrent pin writes while reporting success. Two connects racing produced a confirmation and no record. For a tool whose entire product is a reliable record of what you approved, reporting success without writing is the single worst available failure mode, and it shipped. Use 0.1.2.
The crawler ignored nextCursor, so paginated servers were truncated. I found it, fixed it, and republished every affected record as a floor rather than a count. Then the controlled re-probe on 4 September came back with eighteen servers reporting more tools than I had recorded, and none of those eighteen return nextCursor at all. The extra tools were on page one. Pagination was a real bug that I fixed correctly, and it was not the cause of the numbers I had attributed to it. The actual dependency is the environment the crawl runs under, so the schedule is off until each listing records the conditions it was taken under.
I patched the defect I found, the record looked repaired, and the deeper one was still sitting underneath untouched. That is the second time this month.
The GitHub Action shipped and does not work. Its bootstrap instructions reference a package that is not on npm, and the baseline does not survive the runner. It is now marked experimental with an explicit do-not-adopt line rather than quietly removed, because somebody may already have copied it into a workflow.
A reachable denial of service in the canonicalizer, found during hardening. It is the first thing that touches untrusted input, which is both the worst place for that bug and the most predictable.
What I would change: record the environment before the first crawl, not after the numbers disagree. Every wrong conclusion in the last week came from a measurement whose conditions were not stored alongside it, which is a caveat I have written on the front of other people's numbers and did not apply to my own.