fix(gbrain-status): MCP scoping is per-project, and project-local beats user scope

hasRemoteOnlyGbrainMcp scanned EVERY project's mcpServers in ~/.claude.json,
so one project's remote gbrain registration reclassified broken local engines
as thin-client machine-wide. It now reads user scope plus only the cwd's
nearest-ancestor project key.

The precedence itself was verified empirically and hermetically (fake HOME +
CLAUDE_CONFIG_DIR fixtures, claude 2.1.233): with both scopes defining
gbrain, 'claude mcp get gbrain' reports Scope: Local config — PROJECT-LOCAL
WINS. Both in-repo consumers assumed the opposite; brain-cache's endpoint
resolution flips to nearest-ancestor-project-first, and the stale user-first
pin in brain-cache-roundtrip now pins the verified precedence. (The user-first
jq in the brain-sync preamble resolver gets the same swap in the template
block.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-17 10:43:54 -07:00
co-authored by Claude Fable 5
parent 6955dfa348
commit d8a207fdd7
5 changed files with 272 additions and 48 deletions
+9 -4
View File
@@ -166,7 +166,12 @@ describe('brain-cache endpoint detection', () => {
expect(outer).not.toBe('local');
});
test('detectEndpointHash still prefers user scope over project scope (#2499)', async () => {
test('detectEndpointHash prefers project-local scope over user scope (#2392 wave)', async () => {
// Empirically verified against claude 2.1.233 with hermetic fixtures:
// `claude mcp get gbrain` reports "Scope: Local config" when both scopes
// define the server — project-local WINS. The old pin here encoded the
// opposite (user-first) assumption, which mis-hashed endpoints whenever
// the two scopes disagreed.
const mod = await importCache();
const cj = join(TMP_HOME, 'claude.json');
writeFileSync(cj, JSON.stringify({
@@ -175,14 +180,14 @@ describe('brain-cache endpoint detection', () => {
'/w/repo': { mcpServers: { gbrain: { url: 'https://proj.example/mcp' } } },
},
}));
const userScoped = mod.detectEndpointHash(cj, '/w/repo');
// Same file minus the user-scope entry → different hash proves user scope won.
const conflictHash = mod.detectEndpointHash(cj, '/w/repo');
// Same file minus the USER entry → identical hash proves project scope won.
writeFileSync(cj, JSON.stringify({
projects: {
'/w/repo': { mcpServers: { gbrain: { url: 'https://proj.example/mcp' } } },
},
}));
expect(mod.detectEndpointHash(cj, '/w/repo')).not.toBe(userScoped);
expect(mod.detectEndpointHash(cj, '/w/repo')).toBe(conflictHash);
});
});