What happens
fingerprint_tools hashes the tool object as given. A JSON Schema $ref is a string, so the digest commits to the pointer, not to what it resolves to.
Verified on master:
$ref pointer hashes identically regardless of what it resolves to: True
ref = [{"name": "send_email", "description": "Send an email.",
"inputSchema": {"$ref": "https://attacker.example/schema.json"}}]
The server can change what that URL serves, every time, and the fingerprint never moves.
Why this is different from #142
_meta is an exemption we chose and can withdraw. This one is structural: the digest is sound over the bytes it was given, and the bytes are a pointer. Fingerprinting is being asked to commit to a document it never saw.
It is also not a severity-tuning question, so it does not belong inside the #103 bucket split. A dual-digest design would place $ref in the structural bucket, quietly making the loud half blind to it.
Options
- Resolve refs before fingerprinting. Most faithful, and turns the recorder into something that fetches URLs an untrusted server names. That is the same objection that stops us polling MCP servers on a schedule, so it is probably out.
- Refuse to fingerprint a schema carrying unresolved external refs, and emit
unavailable with a distinct reason, the way a failed listing does since 0.7.0. Honest, and says "we cannot commit to this" rather than committing to nothing.
- Fingerprint what the client resolved, if the client resolved it, since the proxy sits in the path. Best fidelity where available, silent where not.
- Accept and document. Weakest, but at least the limitation would be stated rather than implied, like the tripwire caveat in the README.
Option 2 is my current preference: it converts a silent hole into a visible gap, which is the pattern already used for failed listings and for the four-state coverage attestation.
Local #/$defs/... refs are a separate and easier case, since the target is inside the same document and is already hashed.
Not frozen
core/diagnostics/mcp_schema.py is not part of the ruleset fingerprint.
Source
Reported by @Santoshkumarpuppala in #103.
What happens
fingerprint_toolshashes the tool object as given. A JSON Schema$refis a string, so the digest commits to the pointer, not to what it resolves to.Verified on master:
The server can change what that URL serves, every time, and the fingerprint never moves.
Why this is different from #142
_metais an exemption we chose and can withdraw. This one is structural: the digest is sound over the bytes it was given, and the bytes are a pointer. Fingerprinting is being asked to commit to a document it never saw.It is also not a severity-tuning question, so it does not belong inside the #103 bucket split. A dual-digest design would place
$refin the structural bucket, quietly making the loud half blind to it.Options
unavailablewith a distinct reason, the way a failed listing does since 0.7.0. Honest, and says "we cannot commit to this" rather than committing to nothing.Option 2 is my current preference: it converts a silent hole into a visible gap, which is the pattern already used for failed listings and for the four-state coverage attestation.
Local
#/$defs/...refs are a separate and easier case, since the target is inside the same document and is already hashed.Not frozen
core/diagnostics/mcp_schema.pyis not part of the ruleset fingerprint.Source
Reported by @Santoshkumarpuppala in #103.