]
+ if (!config.url || typeof config.url !== 'string') {
+ setJsonError('Server config must include a "url" field')
+ return null
}
+ if (entries.length <= 1) setJsonError(null)
+ return { name, url: config.url, headers: extractStringHeaders(config.headers) }
+ }
- setJsonError('JSON must contain "mcpServers" or a "url" field')
- return null
- } catch {
- setJsonError('Invalid JSON')
- return null
+ if (parsed.url && typeof parsed.url === 'string') {
+ setJsonError(null)
+ return { name: '', url: parsed.url, headers: extractStringHeaders(parsed.headers) }
}
- },
- []
- )
- const handleTestConnection = useCallback(async () => {
+ setJsonError('JSON must contain "mcpServers" or a "url" field')
+ return null
+ } catch {
+ setJsonError('Invalid JSON')
+ return null
+ }
+ }
+
+ const handleTestConnection = async () => {
if (!isFormValid) return
await testConnection({
@@ -484,15 +492,20 @@ export function McpServerFormModal({
timeout: formData.timeout,
workspaceId,
})
- }, [formData, isFormValid, testConnection, workspaceId])
+ }
- const handleSubmitForm = useCallback(async () => {
+ const handleSubmitForm = async () => {
if (!isFormValid || isDomainBlocked) return
setIsSubmitting(true)
setSubmitError(null)
try {
const headers = headersToRecord(formData.headers)
+ const oauthClientId = formData.oauthClientId?.trim()
+ const oauthClientSecret = formData.oauthClientSecret?.trim()
+ const originalClientId = (originalData.oauthClientId || '').trim()
+ const oauthClientIdChanged = (oauthClientId || '') !== originalClientId
+
const connectionResult = await testConnection({
name: formData.name,
transport: formData.transport,
@@ -503,10 +516,18 @@ export function McpServerFormModal({
})
if (!connectionResult.success) {
- setSubmitError(
- connectionResult.error || 'Connection test failed. Please check the URL and try again.'
- )
- return
+ 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
+ }
}
await onSubmit({
@@ -515,19 +536,30 @@ export function McpServerFormModal({
url: formData.url!,
headers,
timeout: formData.timeout || 30000,
+ oauthClientId:
+ mode === 'edit'
+ ? oauthClientIdChanged
+ ? (oauthClientId ?? '')
+ : undefined
+ : oauthClientId || undefined,
+ oauthClientSecret:
+ mode === 'edit'
+ ? oauthClientSecretTouched
+ ? (oauthClientSecret ?? '')
+ : undefined
+ : oauthClientSecret || undefined,
})
onOpenChange(false)
} catch (error) {
- const message = error instanceof Error ? error.message : 'Failed to save server'
- setSubmitError(message)
+ setSubmitError(toError(error).message || 'Failed to save server')
logger.error('Failed to save MCP server:', error)
} finally {
setIsSubmitting(false)
}
- }, [formData, isFormValid, isDomainBlocked, testConnection, workspaceId, onSubmit, onOpenChange])
+ }
- const handleSubmitJson = useCallback(async () => {
+ const handleSubmitJson = async () => {
const config = parseJsonConfig(jsonInput)
if (!config) return
@@ -570,21 +602,12 @@ export function McpServerFormModal({
onOpenChange(false)
} catch (error) {
- const message = error instanceof Error ? error.message : 'Failed to save server'
- setSubmitError(message)
+ setSubmitError(toError(error).message || 'Failed to save server')
logger.error('Failed to save MCP server from JSON:', error)
} finally {
setIsSubmitting(false)
}
- }, [
- jsonInput,
- parseJsonConfig,
- allowedMcpDomains,
- testConnection,
- workspaceId,
- onSubmit,
- onOpenChange,
- ])
+ }
const isSubmitDisabled =
isSubmitting || !isFormValid || isDomainBlocked || (mode === 'edit' && !hasChanges)
@@ -676,6 +699,52 @@ export function McpServerFormModal({
))}
+
+
+ {showAdvanced && (
+
+
+ {
+ if (testResult) clearTestResult()
+ if (submitError) setSubmitError(null)
+ setFormData((prev) => ({ ...prev, oauthClientId: e.target.value }))
+ }}
+ className='h-9'
+ />
+
+
+ {
+ if (testResult) clearTestResult()
+ if (submitError) setSubmitError(null)
+ setOauthClientSecretTouched(true)
+ setFormData((prev) => ({ ...prev, oauthClientSecret: e.target.value }))
+ }}
+ className='h-9'
+ />
+
+
+ Only needed for servers that don't support automatic client registration.
+
+
+ )}
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx
index 0d2c047549a..063bd8d4b23 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx
@@ -1,7 +1,9 @@
'use client'
-import { useCallback, useEffect, useMemo, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
+import { toError } from '@sim/utils/errors'
+import { useQueryClient } from '@tanstack/react-query'
import { ChevronDown, Plus, Search } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
@@ -13,6 +15,7 @@ import {
ModalFooter,
ModalHeader,
Tooltip,
+ toast,
} from '@/components/emcn'
import { Input } from '@/components/ui'
import { requestJson } from '@/lib/api/client/request'
@@ -28,6 +31,7 @@ import type { McpTransport } from '@/lib/mcp/types'
import {
type McpServer,
type McpTool,
+ mcpKeys,
useAllowedMcpDomains,
useCreateMcpServer,
useDeleteMcpServer,
@@ -35,6 +39,7 @@ import {
useMcpServers,
useMcpToolsQuery,
useRefreshMcpServer,
+ useStartMcpOauth,
useStoredMcpTools,
useUpdateMcpServer,
} from '@/hooks/queries/mcp'
@@ -100,7 +105,10 @@ function ServerListItem({
({transportLabel})
{isRefreshing
? 'Refreshing...'
@@ -132,6 +140,7 @@ interface MCPProps {
export function MCP({ initialServerId }: MCPProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
+ const queryClient = useQueryClient()
const {
data: servers = [],
@@ -145,11 +154,13 @@ export function MCP({ initialServerId }: MCPProps) {
isFetching: toolsFetching,
} = useMcpToolsQuery(workspaceId)
const { data: storedTools = [], refetch: refetchStoredTools } = useStoredMcpTools(workspaceId)
- const forceRefreshTools = useForceRefreshMcpTools()
+ const forceRefreshToolsMutation = useForceRefreshMcpTools()
+ const forceRefreshTools = forceRefreshToolsMutation.mutate
const createServerMutation = useCreateMcpServer()
const deleteServerMutation = useDeleteMcpServer()
const refreshServerMutation = useRefreshMcpServer()
const updateServerMutation = useUpdateMcpServer()
+ const startOauthMutation = useStartMcpOauth()
const availableEnvVars = useAvailableEnvVarKeys(workspaceId)
const { data: allowedMcpDomains = null } = useAllowedMcpDomains()
@@ -162,15 +173,27 @@ export function MCP({ initialServerId }: MCPProps) {
url?: string
timeout?: number
headers?: { key: string; value: string }[]
+ oauthClientId?: string
+ hasOauthClientSecret?: boolean
}
| undefined
>(undefined)
const [searchTerm, setSearchTerm] = useState('')
const [deletingServers, setDeletingServers] = useState>(() => new Set())
+ const [connectingOauthServers, setConnectingOauthServers] = useState>(() => new Set())
+ const oauthPopupIntervalsRef = useRef