fix(plugin): move the MCP manifest off the path Claude Code auto-discovers as project scope - #287
Merged
Merged
Conversation
kevintseng
force-pushed
the
fix/mcp-manifest-and-fail-closed
branch
4 times, most recently
from
September 4, 2026 06:02
e74d7ce to
8235cc5
Compare
…overs as project scope
The repo shipped `.mcp.json` at its root. That single path is two things at
once: the plugin loader's default MCP manifest, where `${CLAUDE_PLUGIN_ROOT}`
resolves, and the path Claude Code auto-discovers as a PROJECT config for
anyone who merely opens this repository, where it does not. Claude Code says so
itself, from the repo root with no plugin installed:
memesh: node ${CLAUDE_PLUGIN_ROOT}/dist/mcp/server.js - Pending approval
[Warning] [memesh] mcpServers.memesh: Missing environment variables: CLAUDE_PLUGIN_ROOT
The server then dies with -32000 Connection closed. `git log -p -- .mcp.json`
dates the placeholder to 310ce58 on 2026-05-13, replacing a working
`npx -y -p @pcircle/memesh memesh-mcp`: three and a half months.
The manifest moves to `.claude-plugin/mcp.json`, declared by
`.claude-plugin/plugin.json`'s `mcpServers`. Three candidate shapes were
installed into throwaway config dirs and all three registered; this one was
chosen because it mirrors the Codex side and keeps a FILE, which `mcpEntry()`,
both packed smokes and doctor already read. `${CLAUDE_PLUGIN_ROOT}` stays — it
is the documented, correct form inside a plugin manifest; the path was wrong,
not the variable. Custom component paths SUPPLEMENT the defaults rather than
replacing them, so declaring the new path is only half the fix: the root file
has to be gone, or both still load.
Second defect, the reason the first survived. doctor's inspectMcpConfig
resolved the entry with an unconditional
`entry.replaceAll('${CLAUDE_PLUGIN_ROOT}', packageRoot)`, so the referenced
file always existed and the check could never fail — a green light guarding
exactly this. It now substitutes only where something actually substitutes
(plugin-marketplace, or a real CLAUDE_PLUGIN_ROOT in the environment) and
otherwise warns that the entry is NOT VERIFIED, with the command that would
verify it. `warn`, not the existing informational "NOT VERIFIED" idiom,
because summarizeOverallStatus filters informational rows out — a row that
cannot move a verdict is the same shape as the defect being fixed. Every
source-checkout `memesh doctor` therefore now reports PASS_WITH_CONCERNS; CI
accepts that (the CLI exits non-zero only on FAIL).
The suite was holding the defect in place: tests/installation.test.ts asserted
`.mcp.json` EXISTS, and release-preconditions plus check-agent-message-sync
required it in their path lists. All three now require its absence and the new
declaration instead.
[Verified-By: claude mcp list from the repo root with a throwaway CLAUDE_CONFIG_DIR -> BEFORE "Missing environment variables: CLAUDE_PLUGIN_ROOT", AFTER "No MCP servers configured", exit=0; local marketplace install of this tree -> "plugin:memesh:memesh: node .../dist/mcp/server.js - Connected", exit=0; node scripts/run-tests-isolated.mjs tests/plugin-mcp-manifest.test.ts tests/installation.test.ts tests/core/doctor.test.ts -> exit=0, "Tests 188 passed (188)"; whole suite -> exit=0, "Test Files 230 passed | 1 skipped (231), Tests 3278 passed | 11 skipped"; break-test A (reintroduce root .mcp.json) -> exit=1, 3 failed + check-agent-message-sync FAIL; break-test B (restore the unconditional substitution) -> exit=1, "Tests 2 failed | 150 passed (152)"; npm run lint 0; npm run typecheck 0; check-doc-claims 0; check-codemap-parity 0; test:packaged 0; test:packaged:upgrade 0; npm run verify:release 0]
CI red on both Windows legs, green on macOS and Linux:
FAIL tests/core/doctor.test.ts > doctor > fails when the MCP config is invalid JSON
AssertionError: expected { id: 'mcp-config', …(7) } to match object { status: 'fail', …(1) }
`inspectMcpConfig`'s fix line names the ABSOLUTE path of the file to edit —
`Fix ${mcpPath} so Claude Code can read the MCP server definition.` — which is
what a user needs, and on Windows `path.join` builds it with backslashes. The
test asserted the literal `'.claude-plugin/mcp.json'`, forward slashes, so it
could only ever pass off Windows.
The assertion now derives its separator the same way the code does. The code is
unchanged: a Windows user should see a Windows path.
Swept the rest of this PR's test files for the same shape — `stringContaining`
on a literal carrying a path separator — and this was the only one.
[Verified-By: node scripts/run-tests-isolated.mjs > /tmp/f287.log 2>&1; echo
exit=$? -> exit=0; "Test Files 230 passed | 1 skipped (231)", "Tests 3283
passed | 11 skipped (3294)", no Errors line]
[Verified-By: npm run verify:release > /tmp/v287.log 2>&1; echo exit=$? ->
exit=0, ending "consumer install has no high-or-worse advisories"]
[Not verified: I cannot run the Windows legs locally. The reason to expect this
fixes them is that the assertion is now built by the same `path.join` call the
code under test uses, so the two cannot disagree about the separator on any
platform. CI on this branch is what settles it.]
…board alarm, clean up a fallback that could never fire
The English fix message for mcp-config.entry-missing escaped the \$ in
\${MCP_PLACEHOLDER}, so it printed that literal text instead of the
resolved path — every one of the 11 translated locales substituted
correctly, only the English source, which is what the terminal actually
shows, was broken.
mcp-config.placeholder-unresolved is not a failure of an npm or
source-checkout install — there is no plugin root to resolve
${CLAUDE_PLUGIN_ROOT} against there, so nothing is broken to act on — but
it was missing from the dashboard's QUIET_WARN_CODES, so every one of
those installs got an alarm banner for a state its own fix text calls
harmless.
inspectMcpConfig's `label` fallback (`relativeManifest ?? '...'`) could
never actually use its fallback value: the only path that makes
relativeManifest null returns before label is ever read. Restructured the
early return so the fallback is gone rather than dead, with no change in
behavior.
Added scripts/qa/verify-dashboard-doctor-banner-i18n.mjs: a real-browser
check (real HTTP server, real Chromium, all 11 locales) that the quiet
code stays quiet and mcp-config.missing actually renders translated, not
as a raw i18n key or a silent English fallback — something the existing
i18n-catalogue tests could not have caught, since they check the
translation table, not what a real bundle renders. Also found and closed
a real test gap: no existing test exercised the "manifest declared but the
file is gone" branch of mcp-config.missing at all.
The prior commit's dist was built before the inspectMcpConfig restructure landed, so CI's committed-build-output check correctly flagged it as stale. No source change; dist now matches src/core/doctor.ts.
kevintseng
force-pushed
the
fix/mcp-manifest-and-fail-closed
branch
from
September 4, 2026 08:42
8235cc5 to
af01fda
Compare
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.
Summary
The repo shipped
.mcp.jsonat its root. That one path is two things at once: the plugin loader's default MCP manifest, where${CLAUDE_PLUGIN_ROOT}resolves, and the path Claude Code auto-discovers as a project config for anyone who merely opens this repository, where it does not. Claude Code says so itself — from the repo root, throwaway config dir, no plugin installed:The server then dies with
-32000 Connection closed.git log -p -- .mcp.jsondates the placeholder to310ce585on 2026-05-13, replacing a workingnpx -y -p @pcircle/memesh memesh-mcp. Three and a half months.The manifest moves to
.claude-plugin/mcp.json, declared by.claude-plugin/plugin.json. Three candidate shapes were installed into throwaway config dirs and all three registered; this one keeps a file, whichmcpEntry(), both packed smokes and doctor already read, and it mirrors the Codex side.${CLAUDE_PLUGIN_ROOT}stays — it is the documented, correct form inside a plugin manifest; the path was wrong, not the variable. Custom component paths supplement the defaults rather than replacing them (plugin-structure/SKILL.md:98), so declaring the new path is only half the fix: the root file has to be gone, or both still load.Second defect — the reason the first survived.
inspectMcpConfigresolved the entry with an unconditionalentry.replaceAll('${CLAUDE_PLUGIN_ROOT}', packageRoot), so the referenced file always existed and the check could never fail. A green light was guarding exactly this. It now substitutes only where something really does substitute (plugin-marketplace, or a realCLAUDE_PLUGIN_ROOTin the environment) and otherwise reports the entry asNOT VERIFIEDwith the command that would verify it — as awarn, not the existinginformational"NOT VERIFIED" idiom, becausesummarizeOverallStatusfilters informational rows out and a row that cannot move a verdict is the same shape as the defect being fixed. Consequence reviewers should expect: every source-checkoutmemesh doctornow reportsPASS_WITH_CONCERNS; CI accepts it (the CLI exits non-zero only on FAIL).The suite was holding the defect in place.
tests/installation.test.tsasserted.mcp.jsonexists;tests/release-preconditions.test.tsandscripts/check-agent-message-sync.mjsrequired it in their path lists. All three now require its absence and the new declaration instead.Type of change
fix) — install pathdocs)Docs synced
CHANGELOG.md,docs/api/API_REFERENCE.md,docs/platforms/README.md,.github/CODEOWNERS,.gitignoretests/dashboard-i18n.test.tsdist/anddashboard/dist/regeneratedVerification
CLAUDE_CONFIG_DIR:claude plugin install memesh@pcircle-memesh→claude mcp list→plugin:memesh:memesh: node …/dist/mcp/server.js - ✔ Connected. That is a completed MCP handshake, not just registration.CLAUDE_PLUGIN_ROOTunset →No MCP servers configured.andls .mcp.json→ no such file..mcp.json→ 3 tests red andcheck-agent-message-sync: FAIL. Removed → 36/36,PASS.Tests 2 failed | 150 passed, including "resolves against CLAUDE_PLUGIN_ROOT when the environment actually provides one" — a pass it had not earned. Restored → 152/152.lint0,typecheck0,check-doc-claims0,check-codemap-parity0,test:packaged0,test:packaged:upgrade0,verify:release0.eg checkPASS at base2d4c0f04—mcp-manifest-not-project-scopedCODE_COMPLETE anddoctor-mcp-check-can-failINTEGRATION_VERIFIED (ui_route: the dashboard bundle carries the new strings), 9 receipts including two known_bad_mutation failure paths.Not proven
claude plugin details <name>reportsMCP servers (0)for every manifest shape, including the inline one —detailsunder-reports;claude mcp listis authoritative. Worth knowing before anyone verifies this with the wrong command.✔ Connectedhandshake ran against this worktree'sdist/via a local marketplace, not a published npm tarball. The packed-artifact smoke covers the tarball's contents; no end-to-end install from the real marketplace was performed.mcp-config.*sentences; no native reviewer checked them.Found while verifying this, not fixed here
scripts/smoke-packed-artifact.mjsspreads...process.envin six places without strippingMEMESH_DIR/MEMESH_DB_PATH, so a maintainer who has them set gets a failing smoke:env -u MEMESH_DIR npm run test:packaged→ exit 0,MEMESH_DIR=… npm run test:packaged→ exit 1 (ENOENT … /mr-XXXX/.memesh/router.token). Same defect class as #271, in the sibling script that PR #280 did not touch. Filed separately.