fix(mcp): drop root .mcp.json so plugin's mcp-search isn't duplicated (#2411)
The repo shipped both a root-level .mcp.json and plugin/.mcp.json with
identical mcp-search launchers — kept in sync by a build-time guard and
a test. The root file was a holdover from when devs working inside the
repo could load mem-search without installing the plugin. With the
plugin universally installed, every plugin user now sees `/doctor` warn:
Plugin (claude-mem @ plugin:claude-mem:mcp-search): MCP server
"mcp-search" skipped — same command/URL as already-configured
"mcp-search"
…because Claude Code dedupes by command and skips the plugin's
namespaced registration. The duplicate is functionally harmless but
suppresses the canonical `plugin:claude-mem:mcp-search` entry.
This removes the root .mcp.json entirely and re-points everything that
referenced it at the bundled plugin copy:
- .mcp.json: deleted
- .codex-plugin/plugin.json: mcpServers → ./plugin/.mcp.json
- package.json: drop .mcp.json from files
- scripts/build-hooks.js: drop root-file requirement + sync check
- scripts/sync-marketplace.cjs: drop syncManagedFiles entry
- src/npx-cli/commands/install.ts: drop from allowedTopLevelEntries
- tests/infrastructure/plugin-distribution.test.ts: drop two tests
enforcing the now-removed root file
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@
|
|||||||
"nodejs"
|
"nodejs"
|
||||||
],
|
],
|
||||||
"skills": "./plugin/skills/",
|
"skills": "./plugin/skills/",
|
||||||
"mcpServers": "./.mcp.json",
|
"mcpServers": "./plugin/.mcp.json",
|
||||||
"hooks": "./plugin/hooks/codex-hooks.json",
|
"hooks": "./plugin/hooks/codex-hooks.json",
|
||||||
"interface": {
|
"interface": {
|
||||||
"displayName": "claude-mem",
|
"displayName": "claude-mem",
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mcp-search": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "sh",
|
|
||||||
"args": [
|
|
||||||
"-c",
|
|
||||||
"_C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; printf '%s\\n' \"$PWD/plugin\" \"$PWD\"; ls -dt \"$HOME/.codex/plugins/cache/claude-mem-local/claude-mem\"/[0-9]*/ \"$HOME/.codex/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ \"$_C/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/thedotmack/plugin\"; } | while IFS= read -r _R; do [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/mcp-server.cjs\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"claude-mem: mcp server not found\" >&2; exit 1; }; exec node \"$_P/scripts/mcp-server.cjs\""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,7 +44,6 @@
|
|||||||
"dist",
|
"dist",
|
||||||
".agents/plugins/marketplace.json",
|
".agents/plugins/marketplace.json",
|
||||||
".codex-plugin",
|
".codex-plugin",
|
||||||
".mcp.json",
|
|
||||||
"plugin/.claude-plugin",
|
"plugin/.claude-plugin",
|
||||||
"plugin/.codex-plugin",
|
"plugin/.codex-plugin",
|
||||||
"plugin/.mcp.json",
|
"plugin/.mcp.json",
|
||||||
|
|||||||
@@ -414,7 +414,6 @@ async function buildHooks() {
|
|||||||
'plugin/.codex-plugin/plugin.json',
|
'plugin/.codex-plugin/plugin.json',
|
||||||
'plugin/.mcp.json',
|
'plugin/.mcp.json',
|
||||||
'.codex-plugin/plugin.json',
|
'.codex-plugin/plugin.json',
|
||||||
'.mcp.json',
|
|
||||||
'.agents/plugins/marketplace.json',
|
'.agents/plugins/marketplace.json',
|
||||||
];
|
];
|
||||||
for (const filePath of requiredDistributionFiles) {
|
for (const filePath of requiredDistributionFiles) {
|
||||||
@@ -433,11 +432,7 @@ async function buildHooks() {
|
|||||||
if (claudeMemMarketplaceEntry?.source?.path !== './plugin') {
|
if (claudeMemMarketplaceEntry?.source?.path !== './plugin') {
|
||||||
throw new Error('.agents/plugins/marketplace.json must point claude-mem source.path at ./plugin so Codex loads the bundled plugin root');
|
throw new Error('.agents/plugins/marketplace.json must point claude-mem source.path at ./plugin so Codex loads the bundled plugin root');
|
||||||
}
|
}
|
||||||
const rootMcp = JSON.parse(fs.readFileSync('.mcp.json', 'utf-8'));
|
|
||||||
const bundledMcp = JSON.parse(fs.readFileSync('plugin/.mcp.json', 'utf-8'));
|
const bundledMcp = JSON.parse(fs.readFileSync('plugin/.mcp.json', 'utf-8'));
|
||||||
if (JSON.stringify(rootMcp.mcpServers?.['mcp-search']) !== JSON.stringify(bundledMcp.mcpServers?.['mcp-search'])) {
|
|
||||||
throw new Error('.mcp.json and plugin/.mcp.json mcp-search launchers must stay in sync');
|
|
||||||
}
|
|
||||||
const mcpSearchCommand = bundledMcp.mcpServers?.['mcp-search']?.args?.join(' ') ?? '';
|
const mcpSearchCommand = bundledMcp.mcpServers?.['mcp-search']?.args?.join(' ') ?? '';
|
||||||
if (!mcpSearchCommand.includes('.codex/plugins/cache/claude-mem-local/claude-mem')) {
|
if (!mcpSearchCommand.includes('.codex/plugins/cache/claude-mem-local/claude-mem')) {
|
||||||
throw new Error('plugin/.mcp.json mcp-search launcher must include Codex cache fallback for hosts that do not inject PLUGIN_ROOT');
|
throw new Error('plugin/.mcp.json mcp-search launcher must include Codex cache fallback for hosts that do not inject PLUGIN_ROOT');
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ function getGitignoreExcludes(basePath) {
|
|||||||
const gitignorePath = path.join(basePath, '.gitignore');
|
const gitignorePath = path.join(basePath, '.gitignore');
|
||||||
if (!existsSync(gitignorePath)) return '';
|
if (!existsSync(gitignorePath)) return '';
|
||||||
|
|
||||||
const syncManagedFiles = new Set([
|
const syncManagedFiles = new Set();
|
||||||
'.mcp.json',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const lines = readFileSync(gitignorePath, 'utf-8').split('\n');
|
const lines = readFileSync(gitignorePath, 'utf-8').split('\n');
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
@@ -527,7 +527,6 @@ function copyPluginToMarketplace(): void {
|
|||||||
const allowedTopLevelEntries = [
|
const allowedTopLevelEntries = [
|
||||||
'.agents',
|
'.agents',
|
||||||
'.codex-plugin',
|
'.codex-plugin',
|
||||||
'.mcp.json',
|
|
||||||
'plugin',
|
'plugin',
|
||||||
'package.json',
|
'package.json',
|
||||||
'package-lock.json',
|
'package-lock.json',
|
||||||
|
|||||||
@@ -90,13 +90,6 @@ describe('Plugin Distribution - Codex Marketplace', () => {
|
|||||||
expect(command).toContain('plugins/cache/thedotmack/claude-mem');
|
expect(command).toContain('plugins/cache/thedotmack/claude-mem');
|
||||||
expect(command).toContain('claude-mem: mcp server not found');
|
expect(command).toContain('claude-mem: mcp server not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps root and bundled MCP launchers in sync', () => {
|
|
||||||
const rootMcp = JSON.parse(readFileSync(path.join(projectRoot, '.mcp.json'), 'utf-8'));
|
|
||||||
const bundledMcp = JSON.parse(readFileSync(path.join(projectRoot, 'plugin/.mcp.json'), 'utf-8'));
|
|
||||||
|
|
||||||
expect(rootMcp.mcpServers['mcp-search']).toEqual(bundledMcp.mcpServers['mcp-search']);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Plugin Distribution - hooks.json Integrity', () => {
|
describe('Plugin Distribution - hooks.json Integrity', () => {
|
||||||
@@ -134,7 +127,7 @@ describe('Plugin Distribution - hooks.json Integrity', () => {
|
|||||||
|
|
||||||
describe('Plugin Distribution - Startup Root Resolution', () => {
|
describe('Plugin Distribution - Startup Root Resolution', () => {
|
||||||
it('MCP startup commands should have config-dir based non-empty fallbacks', () => {
|
it('MCP startup commands should have config-dir based non-empty fallbacks', () => {
|
||||||
for (const relativePath of ['.mcp.json', 'plugin/.mcp.json']) {
|
for (const relativePath of ['plugin/.mcp.json']) {
|
||||||
const command = mcpStartupCommandFrom(relativePath);
|
const command = mcpStartupCommandFrom(relativePath);
|
||||||
|
|
||||||
expect(command).toContain('${CLAUDE_CONFIG_DIR:-$HOME/.claude}');
|
expect(command).toContain('${CLAUDE_CONFIG_DIR:-$HOME/.claude}');
|
||||||
|
|||||||
Reference in New Issue
Block a user