The lock makes the publisher keep the promise, but a client still can't check it, because the hash lives in your repo and the registry entry carries only the version. I'd publish the section hashes with each release. server.json already has room for it: the registry accepts custom metadata under _meta, in the io.modelcontextprotocol.registry/publisher-provided key. An agent host could then compute the same normalised hash from initialize and tools/list on first connect, and refuse or ask for re-approval when it differs from what the registry lists for that version.
That would have caught the instructions move in the worklore case from the client side, without anyone running your replay. It does depend on the normalisation being written down somewhere other than your code, so a host built by someone else hashes the same bytes. A short spec of the canonical form in the mcp-surface README would cover it.
Tried it today. worklore 0.5.1 now publishes the sha256 of every part of the surface a stranger can reproduce without a token (initialize without serverInfo.version, tools/list, and which methods and tools answer anonymously) in server.json under _meta → io.modelcontextprotocol.registry/publisher-provided. The deploy check recomputes them from the live server and compares them with the registry entry, the way a client would, and the README has a 20-line script so anyone can do the same: github.com/worklore/worklore-mcp#v...
One thing I left out on purpose: resources/list. Mine lists stories, so it changes every day, and its hash would break the promise daily without anything actually breaking. Thanks for the idea; it was the missing half.
Data scientist building open-source tools (MCP servers, R packages) for public data — Brazilian government APIs (DataSUS, IBGE, BCB, INEP) and global medical terminologies (WHO, NLM).
Thank you — your cut is the one I used. What goes to the registry is only what a stranger can reproduce without a token: the declared surface, and which methods answer anonymously in the production configuration. The keyed variants stay in the lock, where only I can measure them.
One difference from yours: I kept resources/list in. I measured the seven servers and none of them lists anything that changes, so here it holds still. Your reason for leaving it out is now written into the spec as the rule: a list that changes on its own does not belong in a promise. Your 20-line script became verify.mjs, and it is the second implementation the tests compare against.
That's the best possible outcome for a 20-line script: becoming the second implementation your tests check against. And your reason for keeping resources/list is the right one: the rule should be "only what holds still", not "never resources".
One thought now that there are two of us: is your spec public? If our _meta blocks used the same key and section names, a client could verify any server that publishes them, not just ours. I'm happy to change worklore's format to match yours, since yours has the tests.
Data scientist building open-source tools (MCP servers, R packages) for public data — Brazilian government APIs (DataSUS, IBGE, BCB, INEP) and global medical terminologies (WHO, NLM).
Yes, it's public: SPEC.md, and every registry entry links to it.
I'd love worklore to use the same format. The key stays io.github.sidneybissoli/mcp-surface, with form: "mcp-surface/1" and the declared / anonymous sections. Renaming it would make it a new form and invalidate the hashes already published, so it's the one thing I'd rather not move. §7 has a test vector to check an implementation against, and verify.mjs should then accept worklore's entry as-is.
Two things landed in the spec today that you'll recognise: your "can't say no" probe is step 2 of §6, and §6.2 now says the check holds per response, not per connection.
Done: worklore 0.6.0 publishes mcp-surface/1 under your key, and your verify.mjs accepts it against production:
io.github.worklore/worklore@0.6.0 at worklore.dev/mcp
declared registry d5e4e0a80b32 live d5e4e0a80b32 ok
anonymous 7 methods ok
To follow §2, the stories moved behind a resource template (worklore://story/{slug}), and resources/list now holds three fixed documents: a guide, the story format, and the access rules. I reproduced the §7 vector byte for byte before publishing.
One thing I found along the way: verify.mjs builds the sorted object with Object.fromEntries, and JavaScript then puts integer-like keys first, in numeric order ("9" before "10"), while §3 and RFC 8785 order them as strings ("10" before "9"). Nothing in either of our surfaces has such keys, but a third server's could make the two implementations disagree.
Data scientist building open-source tools (MCP servers, R packages) for public data — Brazilian government APIs (DataSUS, IBGE, BCB, INEP) and global medical terminologies (WHO, NLM).
Confirmed from here: verify.mjs accepts io.github.worklore/worklore@0.6.0 against production, d5e4e0a80b32 on both sides and the 7 anonymous methods as declared. Thank you for publishing it, and for reproducing the §7 vector before you did.
You were right about Object.fromEntries, and the bug was not only in verify.mjs: the package built its hash the same way, so the two agreed with each other and the tests had nothing to catch. Both now write the JSON key by key, so "10" comes before "9" as §3 says. Before changing anything I checked the seven servers: none has such a key in its locked surface, and all seven live endpoints still give their published hashes, so no fingerprint moves. The fix is @sbissoli/mcp-surface 0.5.3.
The SPEC now has a second vector, §7.2, built around exactly this case: a tool whose inputSchema has the properties "10", "9" and "a". Its sha256 is c7255a44a9d70ea6417ae2757c0530895b628551c3ae9fbfc440878a375539b1, and a third implementation (Python, json.dumps with sort_keys) agrees. Running worklore's implementation against it would close the loop on your side too. The note in §3 credits you.
Data scientist building open-source tools (MCP servers, R packages) for public data — Brazilian government APIs (DataSUS, IBGE, BCB, INEP) and global medical terminologies (WHO, NLM).
You were right, and it's done. Thank you — this was the half I was missing.
From today, each release of my seven servers publishes the fingerprint in its MCP Registry entry, under _meta → io.modelcontextprotocol.registry/publisher-provided: the sha256 of the declared surface (initialize, tools, resources, prompts) and which methods answer without a credential on the published endpoint. A host can recompute it on first connect and refuse, or ask again, when it differs from what the registry lists for that version.
On the second point, the canonical form now lives in a SPEC.md rather than only in my code, with a test vector. Writing it down is what exposed two things that would have made a host built by someone else hash different bytes: lists were sorted with localeCompare (locale and ICU dependent, now UTF-16 code-unit order), and the capture didn't follow nextCursor. Neither changed any existing hash. There's also a second implementation, written from the spec with no dependencies (verify.mjs), and the tests require both to agree:
The release pipeline now ends with the same check a client would make: registry entry against the live endpoint, without reading the lock.
What it proves: what you run is what the registry published for that version, and the registry won't let that entry change; with the source, anyone can also check that the hash is the public code's (SPEC §6.1). What it can't prove is that a declared tool is benign — that stays a human decision on re-approval.
One gap left on the host side: the check runs only on first connect. A server can answer tools/list one way at the start of a session and another way twenty minutes in, and the spec only says a server SHOULD send notifications/tools/list_changed when its list changes. A server that wants to switch quietly just won't send it. I'd have the host hash every tools/list response it receives, not only the first, and treat a mid-session mismatch with the registry entry the same as one on connect. Your capture already follows nextCursor, so it's the same function called more often.
SPEC.md seems like the right place to say it: the promise holds per response, not per connection.
Data scientist building open-source tools (MCP servers, R packages) for public data — Brazilian government APIs (DataSUS, IBGE, BCB, INEP) and global medical terminologies (WHO, NLM).
One wrinkle shaped the wording. The registry hash covers the whole surface, never tools/list alone, so a later response can't be hashed by itself. The host swaps the new list into the surface it captured on connect and rehashes all of it. That keeps every hash already published valid, with no new field. The same goes for the other three lists.
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
The lock makes the publisher keep the promise, but a client still can't check it, because the hash lives in your repo and the registry entry carries only the version. I'd publish the section hashes with each release. server.json already has room for it: the registry accepts custom metadata under _meta, in the io.modelcontextprotocol.registry/publisher-provided key. An agent host could then compute the same normalised hash from initialize and tools/list on first connect, and refuse or ask for re-approval when it differs from what the registry lists for that version.
That would have caught the instructions move in the worklore case from the client side, without anyone running your replay. It does depend on the normalisation being written down somewhere other than your code, so a host built by someone else hashes the same bytes. A short spec of the canonical form in the mcp-surface README would cover it.
Tried it today. worklore 0.5.1 now publishes the sha256 of every part of the surface a stranger can reproduce without a token (initialize without serverInfo.version, tools/list, and which methods and tools answer anonymously) in server.json under _meta → io.modelcontextprotocol.registry/publisher-provided. The deploy check recomputes them from the live server and compares them with the registry entry, the way a client would, and the README has a 20-line script so anyone can do the same: github.com/worklore/worklore-mcp#v...
One thing I left out on purpose: resources/list. Mine lists stories, so it changes every day, and its hash would break the promise daily without anything actually breaking. Thanks for the idea; it was the missing half.
Thank you — your cut is the one I used. What goes to the registry is only what a stranger can reproduce without a token: the declared surface, and which methods answer anonymously in the production configuration. The keyed variants stay in the lock, where only I can measure them.
One difference from yours: I kept
resources/listin. I measured the seven servers and none of them lists anything that changes, so here it holds still. Your reason for leaving it out is now written into the spec as the rule: a list that changes on its own does not belong in a promise. Your 20-line script becameverify.mjs, and it is the second implementation the tests compare against.That's the best possible outcome for a 20-line script: becoming the second implementation your tests check against. And your reason for keeping resources/list is the right one: the rule should be "only what holds still", not "never resources".
One thought now that there are two of us: is your spec public? If our _meta blocks used the same key and section names, a client could verify any server that publishes them, not just ours. I'm happy to change worklore's format to match yours, since yours has the tests.
Yes, it's public: SPEC.md, and every registry entry links to it.
I'd love worklore to use the same format. The key stays io.github.sidneybissoli/mcp-surface, with form: "mcp-surface/1" and the declared / anonymous sections. Renaming it would make it a new form and invalidate the hashes already published, so it's the one thing I'd rather not move. §7 has a test vector to check an implementation against, and verify.mjs should then accept worklore's entry as-is.
Two things landed in the spec today that you'll recognise: your "can't say no" probe is step 2 of §6, and §6.2 now says the check holds per response, not per connection.
Done: worklore 0.6.0 publishes mcp-surface/1 under your key, and your verify.mjs accepts it against production:
io.github.worklore/worklore@0.6.0 at worklore.dev/mcp
declared registry d5e4e0a80b32 live d5e4e0a80b32 ok
anonymous 7 methods ok
To follow §2, the stories moved behind a resource template (worklore://story/{slug}), and resources/list now holds three fixed documents: a guide, the story format, and the access rules. I reproduced the §7 vector byte for byte before publishing.
One thing I found along the way: verify.mjs builds the sorted object with Object.fromEntries, and JavaScript then puts integer-like keys first, in numeric order ("9" before "10"), while §3 and RFC 8785 order them as strings ("10" before "9"). Nothing in either of our surfaces has such keys, but a third server's could make the two implementations disagree.
Confirmed from here: verify.mjs accepts io.github.worklore/worklore@0.6.0 against production, d5e4e0a80b32 on both sides and the 7 anonymous methods as declared. Thank you for publishing it, and for reproducing the §7 vector before you did.
You were right about Object.fromEntries, and the bug was not only in verify.mjs: the package built its hash the same way, so the two agreed with each other and the tests had nothing to catch. Both now write the JSON key by key, so "10" comes before "9" as §3 says. Before changing anything I checked the seven servers: none has such a key in its locked surface, and all seven live endpoints still give their published hashes, so no fingerprint moves. The fix is @sbissoli/mcp-surface 0.5.3.
The SPEC now has a second vector, §7.2, built around exactly this case: a tool whose inputSchema has the properties "10", "9" and "a". Its sha256 is c7255a44a9d70ea6417ae2757c0530895b628551c3ae9fbfc440878a375539b1, and a third implementation (Python, json.dumps with sort_keys) agrees. Running worklore's implementation against it would close the loop on your side too. The note in §3 credits you.
You were right, and it's done. Thank you — this was the half I was missing.
From today, each release of my seven servers publishes the fingerprint in its MCP Registry entry, under
_meta→io.modelcontextprotocol.registry/publisher-provided: the sha256 of the declared surface (initialize, tools, resources, prompts) and which methods answer without a credential on the published endpoint. A host can recompute it on first connect and refuse, or ask again, when it differs from what the registry lists for that version.On the second point, the canonical form now lives in a SPEC.md rather than only in my code, with a test vector. Writing it down is what exposed two things that would have made a host built by someone else hash different bytes: lists were sorted with
localeCompare(locale and ICU dependent, now UTF-16 code-unit order), and the capture didn't follownextCursor. Neither changed any existing hash. There's also a second implementation, written from the spec with no dependencies (verify.mjs), and the tests require both to agree:The release pipeline now ends with the same check a client would make: registry entry against the live endpoint, without reading the lock.
What it proves: what you run is what the registry published for that version, and the registry won't let that entry change; with the source, anyone can also check that the hash is the public code's (SPEC §6.1). What it can't prove is that a declared tool is benign — that stays a human decision on re-approval.
One gap left on the host side: the check runs only on first connect. A server can answer tools/list one way at the start of a session and another way twenty minutes in, and the spec only says a server SHOULD send notifications/tools/list_changed when its list changes. A server that wants to switch quietly just won't send it. I'd have the host hash every tools/list response it receives, not only the first, and treat a mid-session mismatch with the registry entry the same as one on connect. Your capture already follows nextCursor, so it's the same function called more often.
SPEC.md seems like the right place to say it: the promise holds per response, not per connection.
Agreed, and it's in the spec now: SPEC §6.2.
One wrinkle shaped the wording. The registry hash covers the whole surface, never tools/list alone, so a later response can't be hashed by itself. The host swaps the new list into the surface it captured on connect and rehashes all of it. That keeps every hash already published valid, with no new field. The same goes for the other three lists.