From e057b73e6ecef3c43f156cd52b8c0e5906dda7d4 Mon Sep 17 00:00:00 2001 From: SEPURI-SAI-KRISHNA Date: Wed, 29 Jul 2026 14:52:57 +0530 Subject: [PATCH] feat(installer): add omitTypeField knob to the mcp-json family --- CHANGELOG.md | 1 + __tests__/custom-targets.test.ts | 32 ++++++++++++++++++++++++ src/installer/targets/custom.ts | 15 +++++++++++ src/installer/targets/mcp-json-family.ts | 13 ++++++++-- 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec70e9aed..07d57abd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ` 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 `, 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 diff --git a/__tests__/custom-targets.test.ts b/__tests__/custom-targets.test.ts index 3d415ab79..8344aae45 100644 --- a/__tests__/custom-targets.test.ts +++ b/__tests__/custom-targets.test.ts @@ -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([]); @@ -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 = { diff --git a/src/installer/targets/custom.ts b/src/installer/targets/custom.ts index 0b4b042f6..92d137230 100644 --- a/src/installer/targets/custom.ts +++ b/src/installer/targets/custom.ts @@ -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 --- /** @@ -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 @@ -283,6 +297,7 @@ function buildFamilyTarget(spec: CustomTargetSpec): AgentTarget { serversKey: spec.serversKey, instructionsFileName: spec.instructionsFileName, absoluteCommand: spec.absoluteCommand, + omitTypeField: spec.omitTypeField, }); } } diff --git a/src/installer/targets/mcp-json-family.ts b/src/installer/targets/mcp-json-family.ts index 2bf1c3cfe..23670d032 100644 --- a/src/installer/targets/mcp-json-family.ts +++ b/src/installer/targets/mcp-json-family.ts @@ -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 { @@ -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; }