|
|
@@ -1,16 +1,20 @@
|
|
|
/**
|
|
|
- * Claude Code target — the historical default. Writes:
|
|
|
+ * Claude Code target. Writes:
|
|
|
*
|
|
|
- * - MCP server entry to `~/.claude.json` (global) or
|
|
|
- * `./.claude.json` (local).
|
|
|
+ * - MCP server entry to `~/.claude.json` (global = user scope, loads
|
|
|
+ * in every project) or `./.mcp.json` (local = project scope, the
|
|
|
+ * file Claude Code actually reads for a single project). See the
|
|
|
+ * scope table at https://code.claude.com/docs/en/mcp.
|
|
|
* - Permissions to `~/.claude/settings.json` (global) or
|
|
|
* `./.claude/settings.json` (local), gated on `autoAllow`.
|
|
|
* - Instructions to `~/.claude/CLAUDE.md` (global) or
|
|
|
* `./.claude/CLAUDE.md` (local).
|
|
|
*
|
|
|
- * All paths and shapes ported verbatim from the original
|
|
|
- * `config-writer.ts` so existing Claude Code installs upgrade in
|
|
|
- * place — no migration on disk required.
|
|
|
+ * Earlier versions wrote the local MCP entry to `./.claude.json` — a
|
|
|
+ * file Claude Code never reads — so the server silently never loaded
|
|
|
+ * until the user manually renamed it to `.mcp.json` (issue #207). We
|
|
|
+ * now write `./.mcp.json` and migrate any stale `./.claude.json` entry
|
|
|
+ * out of the way on install and uninstall.
|
|
|
*/
|
|
|
|
|
|
import * as fs from 'fs';
|
|
|
@@ -45,9 +49,22 @@ function configDir(loc: Location): string {
|
|
|
: path.join(process.cwd(), '.claude');
|
|
|
}
|
|
|
function mcpJsonPath(loc: Location): string {
|
|
|
+ // global → ~/.claude.json (user scope: visible in every project).
|
|
|
+ // local → ./.mcp.json (project scope: the ONLY project-level MCP
|
|
|
+ // file Claude Code reads — NOT ./.claude.json, which it ignores).
|
|
|
return loc === 'global'
|
|
|
? path.join(os.homedir(), '.claude.json')
|
|
|
- : path.join(process.cwd(), '.claude.json');
|
|
|
+ : path.join(process.cwd(), '.mcp.json');
|
|
|
+}
|
|
|
+/**
|
|
|
+ * Where pre-#207 installers wrote the local MCP entry. Claude Code
|
|
|
+ * never reads a project-level `./.claude.json`, so we migrate the
|
|
|
+ * codegraph entry out of it on install and strip it on uninstall.
|
|
|
+ * Only the project-local path is legacy — global `~/.claude.json` is
|
|
|
+ * the correct user-scope location and is left untouched.
|
|
|
+ */
|
|
|
+function legacyLocalMcpPath(): string {
|
|
|
+ return path.join(process.cwd(), '.claude.json');
|
|
|
}
|
|
|
function settingsJsonPath(loc: Location): string {
|
|
|
return path.join(configDir(loc), 'settings.json');
|
|
|
@@ -84,6 +101,14 @@ class ClaudeCodeTarget implements AgentTarget {
|
|
|
// 1. MCP server entry
|
|
|
files.push(writeMcpEntry(loc));
|
|
|
|
|
|
+ // 1b. Migrate away any stale ./.claude.json left by a pre-#207
|
|
|
+ // local install, so the project isn't left with two competing
|
|
|
+ // (one dead) MCP configs.
|
|
|
+ if (loc === 'local') {
|
|
|
+ const migrated = cleanupLegacyLocalMcp();
|
|
|
+ if (migrated) files.push(migrated);
|
|
|
+ }
|
|
|
+
|
|
|
// 2. Permissions (only when autoAllow)
|
|
|
if (opts.autoAllow) {
|
|
|
files.push(writePermissionsEntry(loc));
|
|
|
@@ -112,6 +137,13 @@ class ClaudeCodeTarget implements AgentTarget {
|
|
|
files.push({ path: mcpPath, action: 'not-found' });
|
|
|
}
|
|
|
|
|
|
+ // 1b. Also strip the codegraph entry from a legacy ./.claude.json
|
|
|
+ // so uninstall fully reverses a pre-#207 local install.
|
|
|
+ if (loc === 'local') {
|
|
|
+ const migrated = cleanupLegacyLocalMcp();
|
|
|
+ if (migrated) files.push(migrated);
|
|
|
+ }
|
|
|
+
|
|
|
// 2. Permissions
|
|
|
const settingsPath = settingsJsonPath(loc);
|
|
|
const settings = readJsonFile(settingsPath);
|
|
|
@@ -173,9 +205,10 @@ export function writeMcpEntry(loc: Location): WriteResult['files'][number] {
|
|
|
return { path: file, action: 'unchanged' };
|
|
|
}
|
|
|
// 'created' here means: the file itself did not exist before this
|
|
|
- // write. A pre-existing `.claude.json` containing other MCP servers
|
|
|
- // (no `codegraph` key) is 'updated', not 'created' — we're adding
|
|
|
- // an entry to a file that was already there. Codex uses a different
|
|
|
+ // write. A pre-existing MCP JSON file (`~/.claude.json` globally,
|
|
|
+ // `./.mcp.json` locally) containing other MCP servers (no
|
|
|
+ // `codegraph` key) is 'updated', not 'created' — we're adding an
|
|
|
+ // entry to a file that was already there. Codex uses a different
|
|
|
// idiom (empty-content => 'created') because its config.toml is
|
|
|
// ours alone to manage.
|
|
|
const action: 'created' | 'updated' = before ? 'updated' : (fs.existsSync(file) ? 'updated' : 'created');
|
|
|
@@ -185,6 +218,29 @@ export function writeMcpEntry(loc: Location): WriteResult['files'][number] {
|
|
|
return { path: file, action };
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Strip the codegraph entry from a legacy project-local
|
|
|
+ * `./.claude.json` (written by pre-#207 installers, which Claude Code
|
|
|
+ * never read). Surgical: only our `codegraph` key is removed; sibling
|
|
|
+ * MCP servers and any unrelated keys are preserved, and the file is
|
|
|
+ * deleted only when removal leaves it completely empty. Returns the
|
|
|
+ * file action for reporting, or `null` when there's nothing to migrate.
|
|
|
+ */
|
|
|
+function cleanupLegacyLocalMcp(): WriteResult['files'][number] | null {
|
|
|
+ const file = legacyLocalMcpPath();
|
|
|
+ if (!fs.existsSync(file)) return null;
|
|
|
+ const config = readJsonFile(file);
|
|
|
+ if (!config.mcpServers?.codegraph) return null;
|
|
|
+ delete config.mcpServers.codegraph;
|
|
|
+ if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
|
+ if (Object.keys(config).length === 0) {
|
|
|
+ try { fs.unlinkSync(file); } catch { /* ignore */ }
|
|
|
+ } else {
|
|
|
+ writeJsonFile(file, config);
|
|
|
+ }
|
|
|
+ return { path: file, action: 'removed' };
|
|
|
+}
|
|
|
+
|
|
|
export function writePermissionsEntry(loc: Location): WriteResult['files'][number] {
|
|
|
const file = settingsJsonPath(loc);
|
|
|
const settings = readJsonFile(file);
|