Skip to content

Commit a295d84

Browse files
waleedlatif1claude
andcommitted
fix(mcp): detect OAuth in test-connection and surface structured signal
Test-connection used to attempt an unauthenticated McpClient.connect and surface the resulting transport error to the modal. For spec-compliant OAuth servers (Semrush, Linear, Notion, Atlassian) this produced misleading "Streamable HTTP error: Error POSTing to endpoint:" messages and aborted the create flow before the OAuth dance could start. The form path had a string-regex escape hatch; the JSON path didn't. Replaces the regex with a structured signal — the same `detectMcpAuthType` probe the create flow uses. When the probe returns 'oauth', the route short-circuits with `{ success: false, authRequired: true, authType: 'oauth' }`. Both modal submit paths check `authRequired` explicitly to continue into the normal create + OAuth start flow. Matches the pattern Claude Code / Cursor's MCP clients use: see the OAuth challenge, skip the unauth attempt, run the OAuth handshake. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 37da441 commit a295d84

3 files changed

Lines changed: 37 additions & 19 deletions

File tree

apps/sim/app/api/mcp/servers/test-connection/route.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
validateMcpServerSsrf,
1313
} from '@/lib/mcp/domain-check'
1414
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
15+
import { detectMcpAuthType } from '@/lib/mcp/oauth'
1516
import { resolveMcpConfigEnvVars } from '@/lib/mcp/resolve-config'
1617
import type { McpTransport } from '@/lib/mcp/types'
1718
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
@@ -31,6 +32,13 @@ function isUrlBasedTransport(transport: McpTransport): boolean {
3132
interface TestConnectionResult {
3233
success: boolean
3334
error?: string
35+
/**
36+
* Set when the URL is reachable but responds with an OAuth challenge
37+
* (RFC 9728). The modal uses this to continue into the create + OAuth
38+
* start flow instead of treating the probe failure as fatal.
39+
*/
40+
authRequired?: boolean
41+
authType?: 'none' | 'headers' | 'oauth'
3442
serverInfo?: {
3543
name: string
3644
version: string
@@ -163,6 +171,24 @@ export const POST = withRouteHandler(
163171
}
164172

165173
const result: TestConnectionResult = { success: false }
174+
175+
/**
176+
* Detect OAuth-protected servers (RFC 9728) before attempting an
177+
* unauthenticated connection — for those, an unauthenticated McpClient
178+
* connect will fail with a generic HTTP error, which is misleading.
179+
* The modal uses `authRequired` to continue into the OAuth flow.
180+
*/
181+
const detectedAuthType = await detectMcpAuthType(testConfig.url)
182+
if (detectedAuthType === 'oauth') {
183+
logger.info(`[${requestId}] OAuth challenge detected, skipping unauth connect:`, {
184+
name: body.name,
185+
})
186+
result.authRequired = true
187+
result.authType = 'oauth'
188+
return createMcpSuccessResponse(result, 200)
189+
}
190+
result.authType = detectedAuthType
191+
166192
let client: McpClient | null = null
167193

168194
try {

apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -67,22 +67,6 @@ export interface McpServerFormModalProps {
6767

6868
const ENV_VAR_PATTERN = /\{\{[^}]+\}\}/
6969

70-
/**
71-
* Treats a failed test-connection as a soft failure when the response looks like
72-
* an OAuth/auth challenge — the create flow can then run the probe and kick off
73-
* the OAuth handshake. Without this, OAuth-protected servers like Semrush get
74-
* rejected at the modal before the probe ever runs.
75-
*/
76-
function isAuthRequiredError(errorMessage: string | undefined): boolean {
77-
const text = (errorMessage || '').toLowerCase()
78-
return (
79-
/\b401\b/.test(text) ||
80-
text.includes('unauthorized') ||
81-
text.includes('oauth') ||
82-
text.includes('authentication')
83-
)
84-
}
85-
8670
function hasEnvVarInHostname(url: string): boolean {
8771
const globalPattern = new RegExp(ENV_VAR_PATTERN.source, 'g')
8872
if (url.trim().replace(globalPattern, '').trim() === '') return true
@@ -125,11 +109,12 @@ interface EnvVarDropdownConfig {
125109
}
126110

127111
function getTestButtonLabel(
128-
testResult: { success: boolean; error?: string } | null,
112+
testResult: { success: boolean; error?: string; authRequired?: boolean } | null,
129113
isTestingConnection: boolean
130114
): string {
131115
if (isTestingConnection) return 'Testing...'
132116
if (testResult?.success) return 'Connection success'
117+
if (testResult?.authRequired) return 'Requires OAuth'
133118
if (testResult && !testResult.success) return 'No connection: retry'
134119
return 'Test Connection'
135120
}
@@ -533,7 +518,7 @@ export function McpServerFormModal({
533518
workspaceId,
534519
})
535520

536-
if (!connectionResult.success && !isAuthRequiredError(connectionResult.error)) {
521+
if (!connectionResult.success && !connectionResult.authRequired) {
537522
setSubmitError(
538523
connectionResult.error || 'Connection test failed. Please check the URL and try again.'
539524
)
@@ -595,7 +580,7 @@ export function McpServerFormModal({
595580
workspaceId,
596581
})
597582

598-
if (!connectionResult.success && !isAuthRequiredError(connectionResult.error)) {
583+
if (!connectionResult.success && !connectionResult.authRequired) {
599584
setSubmitError(
600585
connectionResult.error || 'Connection test failed. Please check the URL and try again.'
601586
)

apps/sim/lib/api/contracts/mcp.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,13 @@ export const mcpServerTestResultSchema = z.object({
252252
success: z.boolean(),
253253
message: z.string().optional(),
254254
error: z.string().optional(),
255+
/**
256+
* True when the probe detected an OAuth-protected resource (RFC 9728).
257+
* The modal uses this to skip the failure path and continue into the
258+
* normal create + OAuth start flow.
259+
*/
260+
authRequired: z.boolean().optional(),
261+
authType: mcpAuthTypeSchema.optional(),
255262
serverInfo: z
256263
.object({
257264
name: z.string(),

0 commit comments

Comments
 (0)