From 95f9625079e05943742dbc24255a19b550be6f60 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 11 Jul 2026 10:32:58 -0700 Subject: [PATCH] fix(mcp): coerce corrupted consecutiveFailures instead of crashing the whole server list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/sim/lib/api/contracts/mcp.ts | 7 ++++++- apps/sim/lib/mcp/service.ts | 13 ++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/sim/lib/api/contracts/mcp.ts b/apps/sim/lib/api/contracts/mcp.ts index 1c0159e9ec2..b41cff74d95 100644 --- a/apps/sim/lib/api/contracts/mcp.ts +++ b/apps/sim/lib/api/contracts/mcp.ts @@ -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() diff --git a/apps/sim/lib/mcp/service.ts b/apps/sim/lib/mcp/service.ts index 12c30e7c8c3..7e97d3fc2de 100644 --- a/apps/sim/lib/mcp/service.ts +++ b/apps/sim/lib/mcp/service.ts @@ -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 | null + const currentConfig: McpServerStatusConfig = { + consecutiveFailures: + typeof storedConfig?.consecutiveFailures === 'number' + ? storedConfig.consecutiveFailures + : 0, + lastSuccessfulDiscovery: storedConfig?.lastSuccessfulDiscovery ?? null, + } const now = new Date()