Skip to content

Commit 405f557

Browse files
cdeustclaude
andcommitted
fix(plugin): inline mcpServers in plugin.json, drop repo-root .mcp.json
The repo-root .mcp.json served double duty: plugin MCP config (plugin.json referenced it as "./.mcp.json") AND — unintentionally — project-scoped MCP config picked up by Claude Code whenever this repo was the working directory. Project scope never substitutes ${CLAUDE_PLUGIN_ROOT}, so the spawn ran a literal '<repo>/${CLAUDE_PLUGIN_ROOT}/scripts/launcher.py' → ENOENT → "MCP error -32000: Connection closed", shadowing the healthy plugin-scoped server (plugin:cortex:cortex). Fix: move the server config inline into .claude-plugin/plugin.json mcpServers (documented form) and delete the repo-root .mcp.json — inline plugin config is invisible to project-scope discovery. The contract test now reads the inline object and pins the absence of a repo-root .mcp.json as a regression guard. Verified post-restart: only plugin:cortex:cortex registers; no -32000. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3b40dc9 commit 405f557

5 files changed

Lines changed: 93 additions & 39 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,20 @@
2525
"default": "postgresql://127.0.0.1:5432/cortex"
2626
}
2727
},
28-
"mcpServers": "./.mcp.json",
28+
"mcpServers": {
29+
"cortex": {
30+
"command": "python3",
31+
"args": [
32+
"${CLAUDE_PLUGIN_ROOT}/scripts/launcher.py",
33+
"mcp_server"
34+
],
35+
"env": {
36+
"DATABASE_URL": "${user_config.database_url}",
37+
"CORTEX_RUNTIME": "",
38+
"CORTEX_MEMORY_AP_ENABLED": "1"
39+
}
40+
}
41+
},
2942
"postInstall": {
3043
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/install-plugin.sh",
3144
"message": "Installing Cortex (PostgreSQL + pgvector + Python deps + embedding model) and removing any stale older Cortex installs..."

.mcp.json

Lines changed: 0 additions & 16 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,25 @@ adheres to [Semantic Versioning](https://semver.org/).
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- **`/mcp` showed a failing `cortex` server (-32000) whenever the plugin
12+
source repo itself was the working directory.** The repo-root
13+
`.mcp.json` served double duty: plugin MCP config (plugin.json
14+
referenced it as `"./.mcp.json"`) AND — unintentionally —
15+
project-scoped MCP config picked up by Claude Code when working in
16+
this repo. In project scope `${CLAUDE_PLUGIN_ROOT}` is never
17+
substituted (it is plugin-scope only), so the spawn ran
18+
`python3 '<repo>/${CLAUDE_PLUGIN_ROOT}/scripts/launcher.py'` → ENOENT
19+
→ "MCP error -32000: Connection closed", shadowing the healthy
20+
plugin-scoped server (`plugin:cortex:cortex`, which connected in
21+
~1.7s in the same session's logs). Fix: the MCP server config moved
22+
inline into `.claude-plugin/plugin.json` `mcpServers` (documented
23+
form, plugins-reference) and the repo-root `.mcp.json` was deleted —
24+
inline plugin config is invisible to project-scope discovery. The
25+
contract test now reads the inline object and pins the absence of a
26+
repo-root `.mcp.json`.
27+
928
## [3.19.3] - 2026-06-11
1029

1130
### Fixed

tests_py/scripts/test_launcher_resolution.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
99
Also verifies CLAUDE_PLUGIN_DATA controls deps_dir location.
1010
11-
Source: Discord report 2026-05-09 — `.mcp.json` now uses
12-
${CLAUDE_PLUGIN_ROOT} substitution; we must confirm the launcher
13-
honours it correctly.
11+
Source: Discord report 2026-05-09 — the plugin MCP config (inline in
12+
.claude-plugin/plugin.json) uses ${CLAUDE_PLUGIN_ROOT} substitution;
13+
we must confirm the launcher honours it correctly.
1414
"""
1515

1616
from __future__ import annotations

tests_py/scripts/test_mcp_json_contract.py

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
"""Contract test for .mcp.json — the plugin↔Claude-Code interface.
1+
"""Contract test for the plugin MCP server config — the plugin↔Claude-Code interface.
22
3-
Verifies the `.mcp.json` shape against the documented Claude Code plugin
4-
contract. Source: https://code.claude.com/docs/en/plugins-reference,
3+
Verifies the inline `mcpServers` object in `.claude-plugin/plugin.json`
4+
against the documented Claude Code plugin contract.
5+
Source: https://code.claude.com/docs/en/plugins-reference,
56
section "Environment variables":
67
78
> ${CLAUDE_PLUGIN_ROOT}: ... Both are substituted inline anywhere they
@@ -16,16 +17,29 @@
1617
"env": { "DB_PATH": "${CLAUDE_PLUGIN_ROOT}/data" }
1718
}
1819
19-
Discord 2026-05-09: prior `.mcp.json` used a Python `-c` one-liner that
20+
History of this contract:
21+
22+
Discord 2026-05-09: prior config used a Python `-c` one-liner that
2023
read ~/.claude/plugins/installed_plugins.json and execvp'd into the
2124
launcher. Failure modes were silent because `python3 -c` swallowed stack
2225
traces. The fix routes through the documented substitution mechanism.
2326
24-
This test guards against regression to the inline `-c` script (the
25-
substitutability violation: the inline script imposed a STRONGER
26-
precondition than the contract — it required a specific marketplace
27-
key in installed_plugins.json, rejecting --plugin-dir / --plugin-url /
28-
manual-install scenarios that the documented contract supports).
27+
2026-06-12: the config moved from a repo-root `.mcp.json` (referenced by
28+
plugin.json as "./.mcp.json") to an inline object in plugin.json. Reason:
29+
Claude Code ALSO interprets a repo-root `.mcp.json` as PROJECT-scoped MCP
30+
config when the plugin source repo itself is opened as a working
31+
directory. In project scope `${CLAUDE_PLUGIN_ROOT}` is never substituted
32+
(it is plugin-scope only), so the spawn ran
33+
`python3 '<repo>/${CLAUDE_PLUGIN_ROOT}/scripts/launcher.py'` → ENOENT →
34+
"MCP error -32000: Connection closed" on every session in this repo,
35+
shadowing the healthy plugin-scoped server. Inline plugin.json
36+
`mcpServers` is documented and is invisible to project-scope discovery.
37+
38+
This test guards against regression to either failure mode: the inline
39+
`-c` script (substitutability violation: it imposed a STRONGER
40+
precondition than the contract — a specific marketplace key in
41+
installed_plugins.json) and the reintroduction of a repo-root
42+
`.mcp.json`.
2943
"""
3044

3145
from __future__ import annotations
@@ -36,17 +50,41 @@
3650
import pytest
3751

3852
REPO_ROOT = Path(__file__).resolve().parents[2]
39-
MCP_JSON = REPO_ROOT / ".mcp.json"
53+
PLUGIN_JSON = REPO_ROOT / ".claude-plugin" / "plugin.json"
4054

4155

4256
@pytest.fixture(scope="module")
4357
def mcp_config() -> dict:
44-
return json.loads(MCP_JSON.read_text())
58+
manifest = json.loads(PLUGIN_JSON.read_text())
59+
return manifest["mcpServers"]
60+
61+
62+
def test_mcp_servers_inline_object(mcp_config: dict) -> None:
63+
"""`mcpServers` must be an inline object, not a path string.
4564
65+
A path string ("./.mcp.json") requires a repo-root `.mcp.json`,
66+
which Claude Code double-interprets as project-scoped config when
67+
the plugin source repo is the working directory (see module
68+
docstring, 2026-06-12).
69+
"""
70+
assert isinstance(mcp_config, dict), (
71+
f"mcpServers must be an inline object, got: {type(mcp_config).__name__}"
72+
)
73+
assert "cortex" in mcp_config
4674

47-
def test_mcp_json_exists(mcp_config: dict) -> None:
48-
assert "mcpServers" in mcp_config
49-
assert "cortex" in mcp_config["mcpServers"]
75+
76+
def test_no_repo_root_mcp_json() -> None:
77+
"""A repo-root `.mcp.json` must not exist.
78+
79+
Claude Code picks it up as PROJECT-scoped MCP config in this repo,
80+
where ${CLAUDE_PLUGIN_ROOT} is never substituted — the spawn fails
81+
with -32000 and shadows the healthy plugin-scoped server.
82+
"""
83+
assert not (REPO_ROOT / ".mcp.json").exists(), (
84+
"Repo-root .mcp.json reintroduced — it double-registers as "
85+
"project-scoped config with unsubstituted ${CLAUDE_PLUGIN_ROOT}. "
86+
"Keep the MCP server config inline in .claude-plugin/plugin.json."
87+
)
5088

5189

5290
def test_no_inline_python_c_wrapper(mcp_config: dict) -> None:
@@ -56,9 +94,9 @@ def test_no_inline_python_c_wrapper(mcp_config: dict) -> None:
5694
installed_plugins.json. Manual installs and --plugin-dir runs broke
5795
silently because python3 -c discarded the traceback.
5896
"""
59-
args = mcp_config["mcpServers"]["cortex"]["args"]
97+
args = mcp_config["cortex"]["args"]
6098
assert "-c" not in args, (
61-
"Detected `-c` inline script in .mcp.json args. This swallows "
99+
"Detected `-c` inline script in mcpServers args. This swallows "
62100
"launcher errors and breaks --plugin-dir / manual-install. "
63101
"Use ${CLAUDE_PLUGIN_ROOT}/scripts/launcher.py instead."
64102
)
@@ -71,7 +109,7 @@ def test_args_use_claude_plugin_root_substitution(mcp_config: dict) -> None:
71109
the form ${CLAUDE_PLUGIN_ROOT} are substituted inline before the
72110
process is spawned.
73111
"""
74-
args = mcp_config["mcpServers"]["cortex"]["args"]
112+
args = mcp_config["cortex"]["args"]
75113
joined = " ".join(args)
76114
assert "${CLAUDE_PLUGIN_ROOT}" in joined, (
77115
f"args[] must reference ${{CLAUDE_PLUGIN_ROOT}} for plugin path "
@@ -81,7 +119,7 @@ def test_args_use_claude_plugin_root_substitution(mcp_config: dict) -> None:
81119

82120
def test_launcher_path_referenced(mcp_config: dict) -> None:
83121
"""The args must point at scripts/launcher.py with the mcp_server target."""
84-
args = mcp_config["mcpServers"]["cortex"]["args"]
122+
args = mcp_config["cortex"]["args"]
85123
assert any("scripts/launcher.py" in a for a in args), (
86124
f"Expected scripts/launcher.py in args, got: {args}"
87125
)
@@ -106,5 +144,5 @@ def test_command_is_python3(mcp_config: dict) -> None:
106144
"""`command` must be a python interpreter; the contract requires the
107145
spawned process to be able to execute the launcher.py script.
108146
"""
109-
cmd = mcp_config["mcpServers"]["cortex"]["command"]
147+
cmd = mcp_config["cortex"]["command"]
110148
assert cmd in ("python3", "python"), f"Expected python3 (or python), got: {cmd!r}"

0 commit comments

Comments
 (0)