Repository navigation
Description
The MCP server reports two different versions, and neither has moved since June
initialize is the only moment a client learns what build it is talking to. Ours answers with
a version that is both wrong and stale, in two disagreeing places.
Verified
Two values, one server:
workers/mcp/src/index.ts:43—new McpServer({ name: "ProAgentStore", version: "0.1.0" }, …)
— this is what a connecting client reads fromserverInfo.version.server.json:6—"version": "0.1.1"— this is the public MCP-registry manifest, published by
.github/workflows/publish-mcp-registry.ymlon any push touching that file.
They have never agreed. server.json was last touched by 45c2c4d (2026-06-23), a commit about
parse-tool-calls tests. index.ts's 0.1.0 predates it.
Four commits today changed what the surface serves, and neither version moved:
| SHA | Change |
|---|---|
951ef59 |
every tool re-registered via registerTool; server instructions added |
b2b0ac4 |
readOnlyHint on all 135 tools |
778c5d2 |
destructiveHint on runtime tools |
e392754 |
outputSchema + structuredContent on list_agents / my_instances |
e392754 in particular changed a response shape — my_instances went from a bare array to
{"instances":[…]}. A client that cached the old contract has no signal that anything changed.
Why this is more than hygiene
Hosts cache tool metadata. That is not a hypothesis: the ProAgentStore connector in ChatGPT
served the pre-b2b0ac4 unannotated tool list until manually refreshed, because the annotations
landed with no version change to invalidate against. "Hit Refresh" is currently the only
invalidation mechanism this server offers, and it requires a human who already knows something
shipped.
A monotonic serverInfo.version is the standard signal here, and we serve a constant.
Acceptance criteria
index.ts:43andserver.jsonderive from one source rather than two hand-typed
literals, so they cannot disagree again.- That version changes when the served surface changes. Decide and state what "changes" means —
tool count, annotation set, and declaredoutputSchemaare the candidates, since those are
what a caching host keys on. A commit that only edits a handler's internals need not bump it. - A
docs-drift.mjscheck in its existing style: the advertised version, the manifest version,
and any documented version claim agree. Per ADR 0002 the success line states its denominator,
and the input set is asserted rather than assumed. - The check goes red when the two literals are edited apart, demonstrated.
- Decide whether
publish-mcp-registry.ymlshould fire on an MCP surface change rather than
only on aserver.jsonedit — today the registry entry can only be refreshed by remembering
to touch one file by hand. If the answer is no, say why in the issue.
Verified vs inferred
- Verified: both literals and their
file:line,server.json's last-touching commit and
date, the four SHAs and what each changed, themy_instancesshape change (measured live
2026-08-15), and the registry workflow'spaths:trigger. - Inferred: that a moving
serverInfo.versionwould have caused ChatGPT to re-fetch. The
stale-cache symptom is observed and the absent version signal is verified; that a bump would
specifically have invalidated it is standard host behaviour but was not tested against
OpenAI's client.
Activity
docs(mcp): the docs say what a client actually receives, and a capabi…
fix(mcp): serverInfo.version and server.json read from one constant, …
ci(mcp): the registry publish stops firing on its own file, and asks …
fix(mcp): the version guard reads the copy the public web serves, not…
serge-ivo commentedon Aug 15, 2026
Landed in three commits:
d4efd1d(AC1–4),53b7ed7(AC5 fallout, below),43376c8(a fourth statement of the version the first pass missed). CI green on all three; Deploy MCP Worker green.A measurement that makes this concrete rather than hygienic
GET https://registry.modelcontextprotocol.io/v0/servers?search=ProAgentStorereturns two entries forio.github.ProAgentStore/platform:isLatest0.1.00.1.1So
index.tswas not merely carrying a different number fromserver.json— it was advertising the entry the registry has itself superseded. A client that discovered this server as0.1.1and connected was told byserverInfoit had reached0.1.0. That is a contradiction between two surfaces we both publish, and unlike the caching argument it requires no inference. It also settles which literal wins: the registry's latest, because the alternative downgrades a public artefact. (This corrects a claim in the first draft that0.1.0"was never durably published" — it was.)AC1 — one constant,
MCP_SERVER_VERSIONinworkers/mcp/src/server-version.ts.index.tsimports it;server.jsonand the docs restate it anddocs-driftenforces agreement.index.tsis modelled as a binding, not a restatement, so it fails even when a re-typed literal agrees — two values that happen to match today is the state this issue is about, not the fix.AC2 — the rule is in the constant's docstring, stated as "every field of the published tool definition except
description, plus the serverinstructions" rather than an enumeration, so it stays true when the spec adds a field. Explicitly not a bump: a reworded description (making every wording fix a bump trains people to skip the bump), handler internals, andtitle(derived from the name). #565'susage_summaryrewrite, which landed in this same batch, is therefore correctly not a bump.AC3/AC4 — the check shares one design with #572's; see that issue for the mechanism. Red demo reproduced by me post-rebase, re-typing the literal:
Version not bumped. Reconciling two literals is not the same act as picking a new number.
AC5 — and the red build it produced, which is the sharpest thing here
The answer is no, do not fire on
workers/mcp/**— reasoning now a comment in the workflow. The manifest is a pointer to the endpoint and does not publish the tool list, so a surface change alters nothing in it except the version; and the version cannot move withoutserver.jsonmoving, because the constant is the one source anddocs-driftis a required gate. "The advertised version bumped" and "server.json changed in this push" are the same event.Then the push proved the trigger set was already broken.
d4efd1d's only change to that workflow was the comment block explaining this decision — and that was enough to fire it, because the workflow listed its own path as a trigger:Editing a workflow cannot change the manifest's version, so that trigger could only ever no-op or fail.
53b7ed7removes the self-path, and goes further: the registry is append-only per version, so anyserver.jsonedit that does not move the version — a reworded description, a changed title — fails identically. "Already published" is a correct outcome and must not be a red build, because a workflow that is red for a correct outcome is one people stop reading. The job now asks the registry what it holds and skips when it matches, stating what it measured (name, entries seen, latest published, version wanted) and failing rather than skipping if the registry is unreachable or unparseable —set -euo pipefail,curl --fail,jq -e. Verified live: 2 entries, latest0.1.1, wants0.1.1→ skip; a bumped value still publishes. Confirmed in production: neither of the two subsequent pushes fired the workflow.The first pass missed a fourth statement of the version — same class as check 6's miss
store/.well-known/mcp-server.jsonis a separate committed file with its own hand-typedversion, served publicly at https://proagentstore.online/.well-known/mcp-server.json (workers/host/build.js:155reads it,src/index.ts:204routes it,pages.ts's llms.txt advertises it as "MCP discovery"). It serves0.1.1, verified live — so nothing was broken, and that is the problem: a guard comparing three of four statements and passing is certifying ground it never walked. It is the same miss on the same class of file as the one recorded indocs-drift.mjs's own header, where check 6 printed a green tick over three hand-written paths while proagentstore.online told visitors the server had "~67 tools".43376c8adds it as a table entry, with its own red demonstration.All four public statements now agree at
0.1.1:serverInfo(source),server.json, the served/.well-known/mcp-server.json, and the docs page.Left open — AC2 is stated but not enforced
Nothing fails when the surface moves and the version does not;
docs-driftenforces only that the statements of the version agree. The check that would enforce it is a committed fingerprint over the published tool definitions minusdescription, ratcheted againstMCP_SERVER_VERSION— andconformance.test.tsalready holds exactly the wire objects it would be taken from. Deliberately not built here: it is a second ratchet, it belongs in a file this work did not own, and landing it untested as a rider on this fix is the failure mode ADR 0002 is about. It should be its own issue, and this one stays open until that call is made.Not verified
serverInfo.versionon the wire. UnauthenticatedPOST /mcpinitializereturns401 invalid_token(www-authenticate: Bearer realm="OAuth"), so both the old and new values are read from source. Public MCP facts available for verification are/health({"ok":true,"service":"proagentstore-mcp","tools":135}, green after deploy) and the two manifests. Confirming the advertised value needs one authenticatedinitialize.Also unverified and correctly labelled inferred in the code: that a moving
serverInfo.versionwould specifically have made ChatGPT re-fetch. It is the standard host mechanism; it was not tested against OpenAI's client.fix(guards): the two tool-count constants nothing compared are now co…
feat(mcp): the published surface is hashed against the version that p…
serge-ivo commentedon Aug 15, 2026
AC2 is now built and enforced —
1d5cb24. CI green, Deploy MCP Worker green. This closes the gap I left open earlier today.The two decisions, stated
1. What is in the fingerprint: everything a client receives EXCEPT
description, plusSERVER_INSTRUCTIONS.Not an enumerated field list, deliberately — a list drifts from what the SDK actually publishes, and this issue exists because two hand-maintained restatements of one fact disagreed for two months. The hash is taken over the real
tools/listobjects withdescriptiondeleted, so a field the MCP SDK starts publishing tomorrow is inside the fingerprint the day it appears. In practice that covers the tool name set,inputSchema,annotations,outputSchemaandtitle— exactly whatMCP_SERVER_VERSION's docstring already declared the surface to be, so the rule and the mechanism cannot come apart.descriptionis excluded as a decision, not an oversight, and the code says so: a description is prose a model reads, not a contract a client builds against. #565 rewroteusage_summary's description hours before this landed and correctly did not bump the version — a test asserts that rewording it leaves the hash byte-identical, which is what makes that correct rather than merely tolerated. Making every wording fix a bump trains people to skip the bump.2. On mismatch it FAILS and names the fix. It prints the computed hash and says "bump
MCP_SERVER_VERSION, add a newSURFACE_LOCKentry". It does not auto-bump: a number that changes without anyone deciding it changed is how a version stops meaning anything, which is the state this issue was filed about.It earned itself before it landed
Rebasing onto #574 — which changed the write tools'
inputSchemaand bumpedMCP_SERVER_VERSIONto0.1.2by hand, correctly, hours earlier — failed with:That is the mechanism working on a real bump nobody staged for it. Both entries are kept — only the current version's is compared, but a map that remembers is what makes editing a published version's surface a visible act rather than a one-line "update the fingerprint".
What it guarantees, and what it does not
It makes a surface change impossible to ship silently: the build goes red and the failure names the bump. It does not mechanically prove the version moved, because the entry can be edited in place. That is why the lock is keyed by version rather than being a bare hash. Forcing the pair to move together needs a git-history arm of the kind
check-migrations.mjs --require-historycarries — named in the code as separate work rather than left implied.Home:
conformance.test.ts, and why not a new fileThe hash must be taken over the objects a host actually receives, and that is where those objects are. A separate file would need either a second harness — a reconstruction, which that file's header argues at length is worthless — or an extracted helper that
tsconfig.json'ssrc/**/*.test.tsexclude would not cover, dragging build config into a test's plumbing. Three claims measured off one honest reading of the wire beats three files measuring three different things. The file header now declares the third layer and why it sits there. (workers/mcp/src/instance-tools/was the contended directory;conformance.test.tswas untouched since #561.)Guards, per ADR 0002
undefined === undefined— a check that stopped running, not a green build.locked: PLACEHOLDER); a real surface change (one sentence appended toSERVER_INSTRUCTIONS, version untouched) reproducing the full failure with the bump instruction; and A ticket's reasoning can be written over MCP and never read back #574 above, unplanned.outputSchemaappearing;inputSchemachanged; instructions changed — and that key reorder does not, because a ratchet that cries wolf is one people regenerate without reading.Two other things confirmed live
server-version.ts's "nothing enforces the bump today" is now false and has been replaced with which check enforces which half:docs-driftproves the four statements agree, the lock proves the number has to move.workers/mcp/CLAUDE.mdgained the regeneration instruction.53b7ed7) was validated in both directions within the hour. It skipped correctly on my own pushes (version unchanged), and then published correctly on A ticket's reasoning can be written over MCP and never read back #574's bump — the registry now serves0.1.2asisLatest, alongside the superseded0.1.0and0.1.1.I am leaving this issue open only if you want the git-history arm tracked here; otherwise every stated acceptance criterion (AC1–AC5, plus AC2's enforcement) is now delivered and it can be closed.
fix(guards): the surface lock is append-only, so a silent surface cha…
serge-ivo commentedon Aug 15, 2026
Closing. Every acceptance criterion walked against the code on
maintoday, with the commit that satisfied each.d4efd1dindex.ts:44readsversion: MCP_SERVER_VERSION;server-version.ts:82is the only literal.index.tsis modelled as a binding, not a restatement, so it fails even when a re-typed literal agrees — two values that happen to match is the state this issue is about, not the fix.d4efd1ddecided + stated it;1d5cb24enforced it;6000b97(#576) closed the rewrite holeMCP_SERVER_VERSION's docstring as "every field of a published tool definition exceptdescription, plus the serverinstructions" — a rule, not an enumeration, so it stays true when the spec adds a field. Enforced by a fingerprint over the realtools/listobjects, now append-only.d4efd1d, extended by43376c8✓ wire surface — advertised version: 1 version(s) from 1 code file(s) == server.json (1), store/.well-known/mcp-server.json (1), platform-docs/mcp.md (1), advertised from workers/mcp/src/index.ts. The first pass compared three of four statements —43376c8added the served/.well-knownmanifest, which agreed at the time and was therefore exactly the kind of file that hides.d4efd1dmainjust now: settingserver.jsonto0.1.9gives✗ [wire-surface] advertised version: server.json gives the wrong value for 1 version(s): serverInfo.version: says "0.1.9", the code defines "0.1.2",pnpm docs:driftexit 1. Re-typing the literal into the constructor reds separately withadvertises the literal "0.1.0" instead of MCP_SERVER_VERSION.53b7ed7workers/mcp/**. Reasoning in the workflow: the manifest is a pointer to the endpoint and does not publish the tool list, so a surface change alters nothing in it except the version — and the version cannot move withoutserver.jsonmoving, since the constant is the one source anddocs-driftis a required gate. "The advertised version bumped" and "server.jsonchanged in this push" are the same event.Verified in production
All four statements now agree at
0.1.2—serverInfo(source),server.json, the served https://proagentstore.online/.well-known/mcp-server.json, and the docs page. The public registry'sisLatestis0.1.2, alongside the superseded0.1.0and0.1.1.The AC5 guard was validated in both directions within an hour of landing: it skipped correctly on pushes that did not move the version, and published correctly on #574's bump to
0.1.2. That guard also fixed a red build I caused —d4efd1dtriggered the workflow via its ownpaths:self-reference and gotinvalid version: cannot publish duplicate version, which turned out to be a pre-existing trap that would have fired on the next description typo.What this issue is NOT held open for
The rewrite-in-place gap — that the lock could be edited rather than appended to — was written into
1d5cb24's header as a known limit, filed as #576, and is now closed by6000b97with a CI gate (Surface lock is append-only (#576): success). Before building it, an in-place rewrite plus a realSERVER_INSTRUCTIONSchange was measured passing all twelve other gates, so the gap was real rather than theoretical.One claim that stayed inferred throughout
That a moving
serverInfo.versionwould specifically have made ChatGPT re-fetch. It is the standard host mechanism and the stale-cache symptom was observed, but it was never tested against OpenAI's client — and it is labelled that way inserver-version.tsrather than hardened into a measured cause. The evidence that carried this issue in the end needed no inference at all: the registry held0.1.0and0.1.1simultaneously, and the server was advertising the entry the registry had itself superseded.feat(mcp): a running coding loop can be read while it runs, because t…
fix(mcp): the tool listing cuts prose about tools the agent cannot ru…
docs(mcp): the agent-facing contracts state the surface the code serv…