Skip to content

Commit 233fa69

Browse files
committed
fix(mcp): accept V2-only server shape defined directly under mcp
V1 decode pushes an 'invalid' diagnostic and strips V2-only fields, so V2-shaped servers defined directly under mcp (not under mcp.servers) were silently dropped. Try V1 first with a temp diagnostics array; on failure try ConfigMCP.Server and surface V1's diagnostic only when both fail. Adds a regression test for timeout-as-object (V2-only shape).
1 parent 74106dd commit 233fa69

2 files changed

Lines changed: 38 additions & 2 deletions

File tree

‎packages/core/src/config/normalize.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,22 @@ function normalizeMcp(input: Record<string, unknown>, encoded: Record<string, un
280280
normalizeMcpTimeout(value, timeout, path, diagnostics)
281281
return
282282
}
283-
const server = decodeValue(ConfigMCPV1.Info, value, path, diagnostics)
284-
if (server !== undefined)
283+
// Try V1 first with a temp diagnostics array so a V1 failure doesn't
284+
// pollute diagnostics when the V2 fallback succeeds.
285+
const v1Diag: Diagnostic[] = []
286+
const server = decodeValue(ConfigMCPV1.Info, value, path, v1Diag)
287+
if (server !== undefined) {
285288
setOwn(legacyServers, name, canonical(ConfigMCP.Server, ConfigMigrateV1.migrateMcp(server)))
289+
return
290+
}
291+
// V2-only shape (e.g. disabled/timeout-object) — V1 decode failed, try V2
292+
const v2Server = decodeValue(ConfigMCP.Server, value, path, diagnostics)
293+
if (v2Server !== undefined) {
294+
setOwn(nativeServers, name, v2Server)
295+
return
296+
}
297+
// Both failed — surface the V1 diagnostic (more actionable for legacy users)
298+
diagnostics.push(...v1Diag)
286299
})
287300
}
288301
}

‎packages/core/test/config/normalization.test.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,29 @@ describe("ConfigNormalize", () => {
445445
expect(unknown.diagnostics.map((item) => [item.kind, item.path])).toEqual([["invalid", ["mcp", "timeout"]]])
446446
})
447447

448+
test("accepts V2-only server shape defined directly under mcp (not under mcp.servers)", () => {
449+
// timeout as object is V2-only; V1 expects a PositiveInt and fails, triggering V2 fallback
450+
const result = normalized({
451+
mcp: {
452+
"my-server": { type: "local", command: ["run"], timeout: { catalog: 6000 } },
453+
},
454+
})
455+
expect((result.encoded.mcp as { servers: Record<string, unknown> }).servers).toEqual({
456+
"my-server": { type: "local", command: ["run"], timeout: { catalog: 6000 } },
457+
})
458+
expect(result.diagnostics).toEqual([])
459+
})
460+
461+
test("still rejects truly invalid server shapes under mcp", () => {
462+
const result = normalized({
463+
mcp: {
464+
"bad-server": { unknownField: true },
465+
},
466+
})
467+
expect(result.encoded.mcp).toBeUndefined()
468+
expect(result.diagnostics.some((item) => item.kind === "invalid" && item.path.join(".").startsWith("mcp.bad-server"))).toBe(true)
469+
})
470+
448471
test("merges bounded compaction leaves and omits unsupported leaves", () => {
449472
const result = normalized({
450473
compaction: {

0 commit comments

Comments
 (0)