fix(installer): write Claude project-local MCP config to .mcp.json (#207) (#209)

Project-local installs wrote the MCP server to ./.claude.json, which Claude Code never reads — project-scoped servers must live in .mcp.json. The codegraph tools silently never loaded until users renamed the file by hand. Local installs now write ./.mcp.json and migrate any stale ./.claude.json entry on install and uninstall (siblings preserved). Global installs (~/.claude.json, user scope) were already correct.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-05-20 09:47:03 -05:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 7fe64b32be
commit 79b9601aae
5 changed files with 174 additions and 24 deletions
+13 -13
View File
@@ -48,21 +48,21 @@ describe('Installer Config Writer', () => {
describe('readJsonFile error handling', () => {
it('should return empty object for non-existent file', () => {
// writeMcpConfig reads claude.json - if it doesn't exist, it should create it
// writeMcpConfig reads .mcp.json - if it doesn't exist, it should create it
writeMcpConfig('local');
const claudeJson = path.join(tempDir, '.claude.json');
expect(fs.existsSync(claudeJson)).toBe(true);
const mcpJson = path.join(tempDir, '.mcp.json');
expect(fs.existsSync(mcpJson)).toBe(true);
const content = JSON.parse(fs.readFileSync(claudeJson, 'utf-8'));
const content = JSON.parse(fs.readFileSync(mcpJson, 'utf-8'));
expect(content.mcpServers).toBeDefined();
expect(content.mcpServers.codegraph).toBeDefined();
});
it('should handle corrupted JSON by creating backup', () => {
// Create a corrupted claude.json
const claudeJson = path.join(tempDir, '.claude.json');
fs.writeFileSync(claudeJson, '{ this is not valid json !!!');
// Create a corrupted .mcp.json
const mcpJson = path.join(tempDir, '.mcp.json');
fs.writeFileSync(mcpJson, '{ this is not valid json !!!');
// Suppress console.warn during test
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
@@ -76,28 +76,28 @@ describe('Installer Config Writer', () => {
expect(warnMsg).toContain('Warning');
// Backup should exist
expect(fs.existsSync(claudeJson + '.backup')).toBe(true);
expect(fs.existsSync(mcpJson + '.backup')).toBe(true);
// Original backup content should be the corrupted content
const backup = fs.readFileSync(claudeJson + '.backup', 'utf-8');
const backup = fs.readFileSync(mcpJson + '.backup', 'utf-8');
expect(backup).toContain('this is not valid json');
// New file should be valid JSON with codegraph config
const content = JSON.parse(fs.readFileSync(claudeJson, 'utf-8'));
const content = JSON.parse(fs.readFileSync(mcpJson, 'utf-8'));
expect(content.mcpServers.codegraph).toBeDefined();
warnSpy.mockRestore();
});
it('should preserve existing valid config when adding codegraph', () => {
const claudeJson = path.join(tempDir, '.claude.json');
fs.writeFileSync(claudeJson, JSON.stringify({
const mcpJson = path.join(tempDir, '.mcp.json');
fs.writeFileSync(mcpJson, JSON.stringify({
mcpServers: { other: { command: 'other-tool' } },
customField: 'preserved',
}, null, 2));
writeMcpConfig('local');
const content = JSON.parse(fs.readFileSync(claudeJson, 'utf-8'));
const content = JSON.parse(fs.readFileSync(mcpJson, 'utf-8'));
expect(content.mcpServers.codegraph).toBeDefined();
expect(content.mcpServers.other).toBeDefined();
expect(content.customField).toBe('preserved');