mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-11 15:39:04 +02:00
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:
co-authored by
Claude Fable 5
parent
6955dfa348
commit
d8a207fdd7
@@ -11,7 +11,10 @@
|
||||
* Gate-tier, free, pure import + assertion. Runs in <100ms.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { describe, test, expect, afterAll } from 'bun:test';
|
||||
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import {
|
||||
BRAIN_CACHE_ENTITIES,
|
||||
SKILL_DIGEST_SUBSETS,
|
||||
@@ -167,3 +170,59 @@ describe('brain-cache-spec internal consistency', () => {
|
||||
expect(getPreflightSkills().sort()).toEqual(expected.sort());
|
||||
});
|
||||
});
|
||||
|
||||
describe('brain-cache MCP scope precedence (C15 pin)', () => {
|
||||
// Claude Code resolves a same-name MCP conflict in favor of the
|
||||
// PROJECT-LOCAL entry (.projects[cwd].mcpServers) over the user-scope
|
||||
// entry (.mcpServers). Verified empirically against claude 2.1.233 with a
|
||||
// hermetic fake $HOME: `claude mcp get gbrain` reported "Scope: Local
|
||||
// config" and the project-local URL when both scopes defined gbrain.
|
||||
// detectEndpointHash must hash the endpoint the project actually talks
|
||||
// to, or a brain switch would never invalidate the cache.
|
||||
const TMP = mkdtempSync(join(tmpdir(), 'brain-cache-precedence-'));
|
||||
afterAll(() => rmSync(TMP, { recursive: true, force: true }));
|
||||
|
||||
const cache = () => import('../bin/gstack-brain-cache');
|
||||
const writeFixture = (name: string, cfg: object): string => {
|
||||
const p = join(TMP, name);
|
||||
writeFileSync(p, JSON.stringify(cfg));
|
||||
return p;
|
||||
};
|
||||
const USER_URL = { type: 'http', url: 'https://user.example/mcp' };
|
||||
const PROJ_URL = { type: 'http', url: 'https://proj.example/mcp' };
|
||||
|
||||
test('project-local gbrain entry beats user scope for a cwd inside the project', async () => {
|
||||
const mod = await cache();
|
||||
const conflict = writeFixture('claude-conflict.json', {
|
||||
mcpServers: { gbrain: USER_URL },
|
||||
projects: { '/w/repo': { mcpServers: { gbrain: PROJ_URL } } },
|
||||
});
|
||||
const conflictHash = mod.detectEndpointHash(conflict, '/w/repo/src');
|
||||
// Same hash as the project entry alone → the project-local entry won.
|
||||
const projOnly = writeFixture('claude-proj-only.json', {
|
||||
projects: { '/w/repo': { mcpServers: { gbrain: PROJ_URL } } },
|
||||
});
|
||||
expect(conflictHash).toBe(mod.detectEndpointHash(projOnly, '/w/repo/src'));
|
||||
// And NOT the user entry's hash.
|
||||
const userOnly = writeFixture('claude-user-only.json', {
|
||||
mcpServers: { gbrain: USER_URL },
|
||||
});
|
||||
expect(conflictHash).not.toBe(mod.detectEndpointHash(userOnly, '/w/repo/src'));
|
||||
});
|
||||
|
||||
test('user scope still resolves when the cwd has no project-local entry', async () => {
|
||||
const mod = await cache();
|
||||
const cj = writeFixture('claude-user-fallback.json', {
|
||||
mcpServers: { gbrain: USER_URL },
|
||||
projects: { '/other/repo': { mcpServers: { gbrain: PROJ_URL } } },
|
||||
});
|
||||
const hash = mod.detectEndpointHash(cj, '/w/unrelated');
|
||||
expect(hash).toHaveLength(8);
|
||||
// Matches the user-only hash — the OTHER project's entry is invisible
|
||||
// outside its own tree.
|
||||
const userOnly = writeFixture('claude-user-only-2.json', {
|
||||
mcpServers: { gbrain: USER_URL },
|
||||
});
|
||||
expect(hash).toBe(mod.detectEndpointHash(userOnly, '/w/unrelated'));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user