From 27a0cfdceb5245e7b479f22b464471a939f7b1b8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 May 2026 19:18:36 -0700 Subject: [PATCH 1/2] fix(mcp): probe-based OAuth detection in test-connection --- .../api/mcp/servers/test-connection/route.ts | 13 ++++++++++ .../mcp-server-form-modal.tsx | 26 ++++++++----------- apps/sim/hooks/queries/mcp.ts | 1 + apps/sim/lib/api/contracts/mcp.ts | 2 ++ 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/apps/sim/app/api/mcp/servers/test-connection/route.ts b/apps/sim/app/api/mcp/servers/test-connection/route.ts index bd88be77aa..d078312855 100644 --- a/apps/sim/app/api/mcp/servers/test-connection/route.ts +++ b/apps/sim/app/api/mcp/servers/test-connection/route.ts @@ -12,6 +12,7 @@ import { validateMcpServerSsrf, } from '@/lib/mcp/domain-check' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' +import { detectMcpAuthType } from '@/lib/mcp/oauth' import { resolveMcpConfigEnvVars } from '@/lib/mcp/resolve-config' import type { McpTransport } from '@/lib/mcp/types' import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils' @@ -31,6 +32,8 @@ function isUrlBasedTransport(transport: McpTransport): boolean { interface TestConnectionResult { success: boolean error?: string + authRequired?: boolean + authType?: 'none' | 'headers' | 'oauth' serverInfo?: { name: string version: string @@ -163,6 +166,16 @@ export const POST = withRouteHandler( } const result: TestConnectionResult = { success: false } + + // Skip unauth connect when the server returns an RFC 9728 OAuth challenge. + const detectedAuthType = await detectMcpAuthType(testConfig.url) + if (detectedAuthType === 'oauth') { + result.authRequired = true + result.authType = 'oauth' + return createMcpSuccessResponse(result, 200) + } + result.authType = detectedAuthType + let client: McpClient | null = null try { diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx index c4a27748a4..9ae009c34b 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx @@ -52,6 +52,7 @@ export interface McpServerFormConfig { timeout: number oauthClientId?: string oauthClientSecret?: string + authType?: 'none' | 'headers' | 'oauth' } export interface McpServerFormModalProps { @@ -109,11 +110,12 @@ interface EnvVarDropdownConfig { } function getTestButtonLabel( - testResult: { success: boolean; error?: string } | null, + testResult: { success: boolean; error?: string; authRequired?: boolean } | null, isTestingConnection: boolean ): string { if (isTestingConnection) return 'Testing...' if (testResult?.success) return 'Connection success' + if (testResult?.authRequired) return 'Requires OAuth' if (testResult && !testResult.success) return 'No connection: retry' return 'Test Connection' } @@ -517,19 +519,11 @@ export function McpServerFormModal({ workspaceId, }) - if (!connectionResult.success) { - const errorText = (connectionResult.error || '').toLowerCase() - const looksLikeAuthRequired = - /\b401\b/.test(errorText) || - errorText.includes('unauthorized') || - errorText.includes('oauth') || - errorText.includes('authentication') - if (!looksLikeAuthRequired) { - setSubmitError( - connectionResult.error || 'Connection test failed. Please check the URL and try again.' - ) - return - } + if (!connectionResult.success && !connectionResult.authRequired) { + setSubmitError( + connectionResult.error || 'Connection test failed. Please check the URL and try again.' + ) + return } await onSubmit({ @@ -538,6 +532,7 @@ export function McpServerFormModal({ url: formData.url!, headers, timeout: formData.timeout || 30000, + authType: connectionResult.authType, oauthClientId: mode === 'edit' ? oauthClientIdChanged @@ -587,7 +582,7 @@ export function McpServerFormModal({ workspaceId, }) - if (!connectionResult.success) { + if (!connectionResult.success && !connectionResult.authRequired) { setSubmitError( connectionResult.error || 'Connection test failed. Please check the URL and try again.' ) @@ -600,6 +595,7 @@ export function McpServerFormModal({ url: config.url, headers: config.headers, timeout: 30000, + authType: connectionResult.authType, }) onOpenChange(false) diff --git a/apps/sim/hooks/queries/mcp.ts b/apps/sim/hooks/queries/mcp.ts index 1e583ae5d4..a860ec5933 100644 --- a/apps/sim/hooks/queries/mcp.ts +++ b/apps/sim/hooks/queries/mcp.ts @@ -54,6 +54,7 @@ export interface McpServerInput { enabled: boolean oauthClientId?: string oauthClientSecret?: string + authType?: 'none' | 'headers' | 'oauth' } async function fetchMcpServers(workspaceId: string, signal?: AbortSignal): Promise { diff --git a/apps/sim/lib/api/contracts/mcp.ts b/apps/sim/lib/api/contracts/mcp.ts index 325201d1dc..2a6fc9b088 100644 --- a/apps/sim/lib/api/contracts/mcp.ts +++ b/apps/sim/lib/api/contracts/mcp.ts @@ -252,6 +252,8 @@ export const mcpServerTestResultSchema = z.object({ success: z.boolean(), message: z.string().optional(), error: z.string().optional(), + authRequired: z.boolean().optional(), + authType: mcpAuthTypeSchema.optional(), serverInfo: z .object({ name: z.string(), From 54b6a50a557ca95e4144c4a6fc0dea5de5beca5c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 May 2026 19:29:45 -0700 Subject: [PATCH 2/2] fix(mcp): guard undefined url in probe and use canonical McpAuthType --- .../api/mcp/servers/test-connection/route.ts | 18 ++++++++++-------- .../mcp-server-form-modal.tsx | 4 ++-- apps/sim/hooks/queries/mcp.ts | 10 ++++++++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/api/mcp/servers/test-connection/route.ts b/apps/sim/app/api/mcp/servers/test-connection/route.ts index d078312855..c017de7a34 100644 --- a/apps/sim/app/api/mcp/servers/test-connection/route.ts +++ b/apps/sim/app/api/mcp/servers/test-connection/route.ts @@ -14,7 +14,7 @@ import { import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' import { detectMcpAuthType } from '@/lib/mcp/oauth' import { resolveMcpConfigEnvVars } from '@/lib/mcp/resolve-config' -import type { McpTransport } from '@/lib/mcp/types' +import type { McpAuthType, McpTransport } from '@/lib/mcp/types' import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils' const logger = createLogger('McpServerTestAPI') @@ -33,7 +33,7 @@ interface TestConnectionResult { success: boolean error?: string authRequired?: boolean - authType?: 'none' | 'headers' | 'oauth' + authType?: McpAuthType serverInfo?: { name: string version: string @@ -168,13 +168,15 @@ export const POST = withRouteHandler( const result: TestConnectionResult = { success: false } // Skip unauth connect when the server returns an RFC 9728 OAuth challenge. - const detectedAuthType = await detectMcpAuthType(testConfig.url) - if (detectedAuthType === 'oauth') { - result.authRequired = true - result.authType = 'oauth' - return createMcpSuccessResponse(result, 200) + if (testConfig.url) { + const detectedAuthType = await detectMcpAuthType(testConfig.url) + if (detectedAuthType === 'oauth') { + result.authRequired = true + result.authType = 'oauth' + return createMcpSuccessResponse(result, 200) + } + result.authType = detectedAuthType } - result.authType = detectedAuthType let client: McpClient | null = null diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx index 9ae009c34b..52ad29b301 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx @@ -17,7 +17,7 @@ import { Textarea, } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' -import type { McpTransport } from '@/lib/mcp/types' +import type { McpAuthType, McpTransport } from '@/lib/mcp/types' import { checkEnvVarTrigger, EnvVarDropdown, @@ -52,7 +52,7 @@ export interface McpServerFormConfig { timeout: number oauthClientId?: string oauthClientSecret?: string - authType?: 'none' | 'headers' | 'oauth' + authType?: McpAuthType } export interface McpServerFormModalProps { diff --git a/apps/sim/hooks/queries/mcp.ts b/apps/sim/hooks/queries/mcp.ts index a860ec5933..7652df84e7 100644 --- a/apps/sim/hooks/queries/mcp.ts +++ b/apps/sim/hooks/queries/mcp.ts @@ -22,7 +22,13 @@ import { } from '@/lib/api/contracts/mcp' import { isLoopbackHostname } from '@/lib/core/utils/urls' import { sanitizeForHttp, sanitizeHeaders } from '@/lib/mcp/shared' -import type { McpServerStatusConfig, McpTool, McpTransport, StoredMcpTool } from '@/lib/mcp/types' +import type { + McpAuthType, + McpServerStatusConfig, + McpTool, + McpTransport, + StoredMcpTool, +} from '@/lib/mcp/types' import { workflowMcpServerKeys } from '@/hooks/queries/workflow-mcp-servers' const logger = createLogger('McpQueries') @@ -54,7 +60,7 @@ export interface McpServerInput { enabled: boolean oauthClientId?: string oauthClientSecret?: string - authType?: 'none' | 'headers' | 'oauth' + authType?: McpAuthType } async function fetchMcpServers(workspaceId: string, signal?: AbortSignal): Promise {