fix(mcp): coerce corrupted consecutiveFailures instead of crashing the whole server list (#5593)

Root cause: updateServerStatus() only fell back to the default status
config when the whole statusConfig column was null/undefined, not when
it was a real object missing consecutiveFailures (e.g. the column's
'{}' default on server creation). currentConfig.consecutiveFailures
was then undefined, undefined + 1 evaluated to NaN, and
JSON.stringify(NaN) persisted as a literal `null` into the DB the
first time a freshly-created server had a connection failure.

That corrupted value then failed listMcpServersContract's Zod parse
client-side (consecutiveFailures: z.number() rejects null), and since
the response is a single array, one bad server blanked the entire MCP
servers list with "Response failed contract validation" for the whole
workspace — currently affecting 81 servers across 69 production
workspaces.

Two fixes:
- service.ts: normalize the read-back statusConfig so
  consecutiveFailures is always a real number, never NaN, going
  forward.
- contracts/mcp.ts: coerce any non-number consecutiveFailures
  (including the already-corrupted `null` rows) to the schema's
  default of 0 instead of failing validation, so every
  already-affected workspace self-heals on next load with no DB
  migration needed.
This commit is contained in:
Waleed
2026-07-11 10:37:56 -07:00
committed by GitHub
parent 2ba0b5837d
commit 3d02bbbe62
2 changed files with 14 additions and 6 deletions
+6 -1
View File
@@ -38,9 +38,14 @@ export const mcpTransportSchema = z.enum(['streamable-http'])
export const mcpAuthTypeSchema = z.enum(['none', 'headers', 'oauth'])
const consecutiveFailuresSchema = z.preprocess(
(value) => (typeof value === 'number' ? value : undefined),
z.number().default(0)
)
export const mcpServerStatusConfigSchema = z
.object({
consecutiveFailures: z.number().default(0),
consecutiveFailures: consecutiveFailuresSchema,
lastSuccessfulDiscovery: z.string().nullable().default(null),
})
.passthrough()
+8 -5
View File
@@ -328,11 +328,14 @@ class McpService {
)
.limit(1)
const currentConfig: McpServerStatusConfig =
(currentServer?.statusConfig as McpServerStatusConfig | null) ?? {
consecutiveFailures: 0,
lastSuccessfulDiscovery: null,
}
const storedConfig = currentServer?.statusConfig as Partial<McpServerStatusConfig> | null
const currentConfig: McpServerStatusConfig = {
consecutiveFailures:
typeof storedConfig?.consecutiveFailures === 'number'
? storedConfig.consecutiveFailures
: 0,
lastSuccessfulDiscovery: storedConfig?.lastSuccessfulDiscovery ?? null,
}
const now = new Date()