Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### New Features

- You can now wire CodeGraph into agents that don't have a built-in installer target. `codegraph targets add <spec.json>` registers a custom target from a small declarative spec — pick one of three supported config styles (opencode-style `.jsonc`, Codex-style TOML, or the standard `mcpServers` JSON used by Claude Code / Cursor / Gemini) and name the agent's config paths — and from then on `codegraph install --target <id>`, auto-detection, the interactive agent picker, and `codegraph uninstall` all treat it exactly like a built-in. `codegraph targets list` and `codegraph targets remove` manage registrations. This covers opencode forks such as CoDev Code (#1272), TOML-config agents such as Grok Build (#1324, #1396), and Gemini-style agents such as Qwen Code (#968) without waiting for a new CodeGraph release.
- Custom `mcpServers`-style targets can now omit the `type` field from the server entry, covering agents such as Windsurf whose config expects a `command`/`args`-only entry (#952).

## [1.5.0] - 2026-07-21

Expand Down
32 changes: 32 additions & 0 deletions __tests__/custom-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ describe('Custom targets', () => {
expect(validateCustomTargetSpec({ ...GROK_SPEC, absoluteCommand: true }, BUILTIN_IDS)).not.toEqual([]);
});

it('restricts omitTypeField to booleans on the mcp-json family', () => {
expect(validateCustomTargetSpec({ ...MCP_JSON_SPEC, omitTypeField: true }, BUILTIN_IDS)).toEqual([]);
expect(validateCustomTargetSpec({ ...MCP_JSON_SPEC, omitTypeField: 'yes' }, BUILTIN_IDS)).not.toEqual([]);
expect(validateCustomTargetSpec({ ...GROK_SPEC, omitTypeField: true }, BUILTIN_IDS)).not.toEqual([]);
});

it('rejects path shapes that could escape the agent config dir', () => {
expect(validateCustomTargetSpec({ id: 'x', family: 'opencode', appName: '../evil' }, BUILTIN_IDS)).not.toEqual([]);
expect(validateCustomTargetSpec({ id: 'x', family: 'opencode', appName: 'a/b' }, BUILTIN_IDS)).not.toEqual([]);
Expand Down Expand Up @@ -592,6 +598,32 @@ describe('Custom targets', () => {
});
});

describe('omitTypeField (Windsurf-style no-type entry, #952)', () => {
it('drops the type field when set; keeps type: stdio by default', () => {
writeSpecs(specFile, [
{ ...MCP_JSON_SPEC, id: 'notype', configDir: '~/.notype', omitTypeField: true },
MCP_JSON_SPEC,
]);
getTarget('notype')!.install('global', { autoAllow: false });
getTarget('myagent')!.install('global', { autoAllow: false });

const lean = JSON.parse(fs.readFileSync(path.join(tmpHome, '.notype', 'settings.json'), 'utf-8'));
expect(lean.mcpServers.codegraph).toEqual({ command: 'codegraph', args: ['serve', '--mcp'] });
expect('type' in lean.mcpServers.codegraph).toBe(false);

const standard = JSON.parse(fs.readFileSync(path.join(tmpHome, '.myagent', 'settings.json'), 'utf-8'));
expect(standard.mcpServers.codegraph.type).toBe('stdio');
});

it('printConfig reflects the leaner shape', () => {
writeSpecs(specFile, [{ ...MCP_JSON_SPEC, id: 'notype', configDir: '~/.notype', omitTypeField: true }]);
const printed = getTarget('notype')!.printConfig('global');
const entry = JSON.parse(printed.split('\n\n')[1]!).mcpServers.codegraph;
expect(entry.type).toBeUndefined();
expect(entry.command).toBe('codegraph');
});
});

describe('spec notes (Windsurf-style refresh quirks, #952)', () => {
const NOTE = "Windsurf doesn't reload mcp_config.json live — open the MCP panel and hit Refresh.";
const spec: CustomTargetSpec = {
Expand Down
15 changes: 15 additions & 0 deletions src/installer/targets/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ export interface CustomTargetSpec {
* treatment). No-op on other platforms.
*/
absoluteCommand?: boolean;
/**
* Omit the `type: "stdio"` key from the server entry — for editors
* (Windsurf, #952) that document `{ command, args }`-only stdio
* entries. mcp-json family only. Default false.
*/
omitTypeField?: boolean;

// --- any family ---
/**
Expand Down Expand Up @@ -206,6 +212,14 @@ export function validateCustomTargetSpec(spec: unknown, builtinIds: readonly str
}
}

if (s.omitTypeField !== undefined) {
if (typeof s.omitTypeField !== 'boolean') {
errors.push(`"omitTypeField" must be a boolean (got ${JSON.stringify(s.omitTypeField)})`);
} else if (s.family !== 'mcp-json') {
errors.push(`"omitTypeField" is only supported by the mcp-json family (got family ${JSON.stringify(s.family)})`);
}
}

if (s.notes !== undefined) {
const ok = Array.isArray(s.notes)
&& s.notes.length <= 5
Expand Down Expand Up @@ -283,6 +297,7 @@ function buildFamilyTarget(spec: CustomTargetSpec): AgentTarget {
serversKey: spec.serversKey,
instructionsFileName: spec.instructionsFileName,
absoluteCommand: spec.absoluteCommand,
omitTypeField: spec.omitTypeField,
});
}
}
Expand Down
13 changes: 11 additions & 2 deletions src/installer/targets/mcp-json-family.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ export interface McpJsonFamilySpec {
* other platforms and when resolution fails).
*/
absoluteCommand?: boolean;
/**
* Omit the `type: "stdio"` key from the server entry. A few editors
* (Windsurf, #952) document their stdio MCP entries as
* `{ command, args }` only and can reject an unexpected `type` key —
* this writes the leaner shape (the same one the built-in Antigravity
* target uses). Default false: the entry keeps `type: "stdio"`.
*/
omitTypeField?: boolean;
}

class McpJsonFamilyTarget implements AgentTarget {
Expand All @@ -98,9 +106,10 @@ class McpJsonFamilyTarget implements AgentTarget {
return this.spec.serversKey ?? 'mcpServers';
}

private serverEntry(): { type: string; command: string; args: string[] } {
const entry = getMcpServerConfig();
private serverEntry(): { type?: string; command: string; args: string[] } {
const entry: { type?: string; command: string; args: string[] } = getMcpServerConfig();
if (this.spec.absoluteCommand) entry.command = resolveCodegraphCommand();
if (this.spec.omitTypeField) delete entry.type;
return entry;
}

Expand Down