fix(mcp): announce this package's version in serverInfo, not the SDK's - #9
Merged
Conversation
`FastMCP.__init__` takes no `version`, so `Server.version` stays None and the SDK substitutes
its own release number:
server_version = self.version if self.version else pkg_version("mcp")
Measured on this server before this change (2026-08-25, real stdio handshake, not a read of the
source): it announced `{"name": "yeoul", "version": "1.27.2"}` — the installed `mcp` release, not
yeoul-mcp's `0.1.0`. So the number moved with whatever SDK a machine happened to have and never
with this package: the same yeoul build identifies itself differently on two machines, and a
client using serverInfo to tell builds apart was reading the wrong package's version. This is a
FastMCP default, not a local typo — a separate measurement found the same shape on every
unpatched FastMCP server on this machine.
The fix sets `_mcp_server.version` from the source `__version__`. Deliberately NOT
`importlib.metadata.version("yeoul-mcp")`: under an editable install the dist-info can carry a
stale number, which would trade one wrong value for another that merely looks right.
The import is guarded because both entry paths are real: the console script and the tests import
the package, while `python yeoul_mcp/server.py` runs the file with the package's parent off
sys.path. Both arms were run and both now announce `0.1.0`.
Test, because a documented fix with no test is a promise. `mcp/tests/test_serverinfo_version.py`
launches the server and reads the actual handshake — the existing `mcp` CI job imports the module
and counts tools, which proves the module loads and never reads serverInfo at all. It carries a
negative control: a bare unpatched `FastMCP` must still show `version is None`, so the file cannot
go green while asserting nothing. Reverting the fix turns 4 of its 6 checks red (verified).
Two defects in that test were caught by re-running it before pushing rather than trusting the
first green:
1. It depended on the working directory. `sys.path.insert` fixes the test process, not the
child; `python -c` puts the CALLER's cwd on the child's path, so the handshake answered from
`mcp/` and died from the repo root — which is exactly where CI invokes it. The child's
PYTHONPATH is now pinned to the package parent. Verified from four directories.
2. When the handshake died, two checks vanished and the summary read "3/4 checks passed" — a
shrinking denominator that looks almost healthy. Checks that do not run now declare
themselves (`skip`), and the summary refuses any run whose checks are not all accounted for:
"6 checks accounted for, 7 expected — one vanished without declaring itself" (verified by
forcing the count).
Suites at this commit, run from the repo root: serverInfo 6/6 · _run contract 8/8 · gates 61/61 ·
pre-publish 43 files clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bhyi4
added a commit
that referenced
this pull request
Aug 28, 2026
Until now no version number identified a build. `CHANGELOG.md` has said `[0.1.0] — unreleased` since the first commit on 2026-07-21 while eighteen more went in; there are no tags and no releases. That was nobody's decision — the number simply carried no weight, so nothing exposed its absence. #9 gave it weight. Once the server announces its own version in `serverInfo`, that string is what a client reads to tell builds apart — and it stayed `0.1.0` across the fix, so the server that reported the SDK's version and the server that reports its own are indistinguishable by the very field the fix repaired. Bumping is the other half of #9, not bookkeeping. 0.2.0, not 0.1.1: #10 changed behaviour for anyone using both tools — `arc-prereg` now refuses an arc whose spec is blank, and every close writes a `Second key` line into its `_SUMMARY`. Also closes the packaging door on the same defect. The version lives in two files: `pyproject.toml` is what pip records and what a release is cut from, `yeoul_mcp/__init__.py` is what the server speaks. Every existing check compared the server against `__init__.py`, so the two could drift with the suite still green — measured: setting pyproject alone to 9.9.9 kept it at 6/6. A check now fails when they disagree, verified by forcing the drift. That belongs in the suite rather than in a release checklist nobody runs, because cutting a release is exactly when the two get touched. The denominator guard added in #9 earned its place immediately: this commit added two checks and bumped the expected count by one, and the run refused itself with "8 checks accounted for, 7 expected" rather than reporting a green 8/8. Suites from the repo root: serverInfo 8/8 · _run contract 8/8 · gates 72/72 · pre-publish 43 files clean. Co-authored-by: Mother Seara <seara@bhyi4.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
FastMCP.__init__takes noversion, soServer.versionstaysNoneand the SDK fillsserverInfowith its own release number:Measured on this server before the fix — a real stdio handshake, not a read of the source:
yeoul-mcpis0.1.0. So the announced number tracked whatever SDK a machine happened to have and never tracked this package: the same yeoul build identifies itself differently on two machines, and a client usingserverInfoto tell builds apart was reading the wrong package's version.This is a FastMCP default rather than a local typo — the same shape was found on every unpatched FastMCP server on the machine where this was measured.
The fix
Set
_mcp_server.versionfrom the source__version__. Deliberately notimportlib.metadata.version("yeoul-mcp"): under an editable install the dist-info can carry a stale number, which trades one wrong value for another that merely looks right.The import is guarded because both entry paths are real — the console script and the tests import the package, while
python yeoul_mcp/server.pyruns the file with the package's parent offsys.path. Both arms were run; both now announce0.1.0.Test
A documented fix with no test is a promise. The existing
mcpCI job imports the module and counts tools, which proves the module loads and never readsserverInfoat all.mcp/tests/test_serverinfo_version.pylaunches the server and reads the actual handshake, and is wired into CI.It carries a negative control: a bare unpatched
FastMCPmust still showversion is None. Without that arm the file could go green while asserting nothing.Verified by reverting the fix: 4 of 6 checks turn red.
Two defects in that test, caught before pushing
Re-running it rather than trusting the first green turned up both:
sys.path.insertfixes the test process, not the child;python -cputs the caller's cwd on the child's path — so the handshake answered frommcp/and died from the repo root, which is exactly where CI invokes it. The child'sPYTHONPATHis now pinned to the package parent. Verified from four directories.3/4 checks passed— which looks almost healthy. Checks that do not run now declare themselves, and the summary refuses any run whose checks are not all accounted for:SERVERINFO TESTS FAILED: 6 checks accounted for, 7 expected -- one vanished without declaring itself(verified by forcing the count).Suites at this commit (from the repo root)
_runcontract