Skip to content

Commit 20fb108

Browse files
waleedlatif1claude
andcommitted
fix(mcp): keep OAuth spinner until popup closes; remove dead comments
Move popup-opening into the mutation result so the caller can track its lifecycle. The 'Connecting…' spinner now stays until the user dismisses or completes the OAuth popup, preventing accidental double-clicks that would re-navigate the in-flight popup and invalidate state. Auto-OAuth after server creation now uses the same shared helper for consistent visual feedback. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 311827c commit 20fb108

4 files changed

Lines changed: 54 additions & 50 deletions

File tree

apps/sim/app/api/mcp/servers/[id]/route.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export const PATCH = withRouteHandler(
5353
}
5454
)
5555

56-
// Remove workspaceId from body to prevent it from being updated
5756
const { workspaceId: _, oauthClientSecret, ...updateData } = body
5857
const finalUpdateData: Record<string, unknown> = { ...updateData }
5958
if (oauthClientSecret !== undefined) {
@@ -88,7 +87,6 @@ export const PATCH = withRouteHandler(
8887
}
8988
}
9089

91-
// Get the current server to check if URL or OAuth credentials are changing
9290
const [currentServer] = await db
9391
.select({
9492
url: mcpServers.url,

apps/sim/app/api/mcp/servers/route.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,7 @@ export const POST = withRouteHandler(
243243
transport: body.transport,
244244
workspaceId,
245245
})
246-
} catch (_e) {
247-
// Silently fail
248-
}
246+
} catch (_e) {}
249247

250248
const sourceParam = body.source as string | undefined
251249
const source =

apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,38 @@ export function MCP({ initialServerId }: MCPProps) {
215215
>({})
216216
const [expandedTools, setExpandedTools] = useState<Set<string>>(() => new Set())
217217

218+
const startOauthForServer = useCallback(
219+
async (serverId: string) => {
220+
setConnectingOauthServers((prev) => new Set(prev).add(serverId))
221+
const clear = () =>
222+
setConnectingOauthServers((prev) => {
223+
const next = new Set(prev)
224+
next.delete(serverId)
225+
return next
226+
})
227+
try {
228+
const result = await startOauthMutation.mutateAsync({ serverId, workspaceId })
229+
if (result.status === 'already_authorized') {
230+
clear()
231+
return
232+
}
233+
const { popup } = result
234+
const interval = window.setInterval(() => {
235+
if (popup.closed) {
236+
window.clearInterval(interval)
237+
clear()
238+
}
239+
}, 500)
240+
} catch (e) {
241+
clear()
242+
logger.error('Failed to start MCP OAuth', e)
243+
toast.error(e instanceof Error ? e.message : 'Failed to start authorization')
244+
}
245+
},
246+
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutateAsync is stable
247+
[workspaceId]
248+
)
249+
218250
const handleRemoveServer = useCallback((serverId: string, serverName: string) => {
219251
setServerToDelete({ id: serverId, name: serverName })
220252
setShowDeleteDialog(true)
@@ -465,24 +497,7 @@ export function MCP({ initialServerId }: MCPProps) {
465497
size='sm'
466498
disabled={connectingOauthServers.has(server.id)}
467499
onClick={async () => {
468-
setConnectingOauthServers((prev) => new Set(prev).add(server.id))
469-
try {
470-
await startOauthMutation.mutateAsync({
471-
serverId: server.id,
472-
workspaceId,
473-
})
474-
} catch (e) {
475-
logger.error('Failed to start MCP OAuth', e)
476-
toast.error(
477-
e instanceof Error ? e.message : 'Failed to start authorization'
478-
)
479-
} finally {
480-
setConnectingOauthServers((prev) => {
481-
const next = new Set(prev)
482-
next.delete(server.id)
483-
return next
484-
})
485-
}
500+
await startOauthForServer(server.id)
486501
}}
487502
>
488503
{connectingOauthServers.has(server.id) ? 'Connecting…' : 'Connect with OAuth'}
@@ -745,15 +760,7 @@ export function MCP({ initialServerId }: MCPProps) {
745760
config: { ...config, enabled: true },
746761
})
747762
if (result.authType === 'oauth') {
748-
try {
749-
await startOauthMutation.mutateAsync({
750-
serverId: result.serverId,
751-
workspaceId,
752-
})
753-
} catch (e) {
754-
logger.error('Failed to start MCP OAuth', e)
755-
toast.error(e instanceof Error ? e.message : 'Failed to start authorization')
756-
}
763+
await startOauthForServer(result.serverId)
757764
}
758765
}}
759766
workspaceId={workspaceId}

apps/sim/hooks/queries/mcp.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
type McpServerTestResult,
1616
type RefreshMcpServerResult,
1717
refreshMcpServerContract,
18-
type StartMcpOauthResult,
1918
startMcpOauthContract,
2019
testMcpServerConnectionContract,
2120
updateMcpServerContract,
@@ -170,21 +169,23 @@ export function useCreateMcpServer() {
170169
}
171170

172171
/**
173-
* Mutation that begins the OAuth flow for an MCP server. Calls
174-
* `/api/mcp/oauth/start`, opens the upstream authorization URL in a popup
175-
* when the server requires user consent, and resolves to the typed result.
176-
*
177-
* `data.status === 'redirect'` means a popup was opened; the caller should
178-
* wait for the `mcp-oauth` postMessage from the callback page.
179-
* `data.status === 'already_authorized'` means valid tokens already exist.
172+
* Result of `useStartMcpOauth`. When `popup` is set, the caller should wait
173+
* for it to close (or for the `mcp-oauth` postMessage) before clearing any
174+
* "connecting" UI state.
180175
*/
176+
export type StartMcpOauthMutationResult =
177+
| { status: 'redirect'; popup: Window }
178+
| { status: 'already_authorized' }
179+
181180
export function useStartMcpOauth() {
182-
return useMutation<StartMcpOauthResult, Error, { serverId: string; workspaceId: string }>({
183-
mutationFn: async ({ serverId, workspaceId }) => {
184-
const result = await requestJson(startMcpOauthContract, {
185-
query: { serverId, workspaceId },
186-
})
187-
if (result.status === 'redirect') {
181+
return useMutation<StartMcpOauthMutationResult, Error, { serverId: string; workspaceId: string }>(
182+
{
183+
mutationFn: async ({ serverId, workspaceId }) => {
184+
const result = await requestJson(startMcpOauthContract, {
185+
query: { serverId, workspaceId },
186+
})
187+
if (result.status === 'already_authorized') return { status: 'already_authorized' }
188+
188189
const parsedUrl = new URL(result.authorizationUrl)
189190
if (parsedUrl.protocol !== 'https:') {
190191
throw new Error('Authorization URL must use HTTPS')
@@ -197,10 +198,10 @@ export function useStartMcpOauth() {
197198
if (!popup) {
198199
throw new Error('Popup blocked. Please allow popups for this site and retry.')
199200
}
200-
}
201-
return result
202-
},
203-
})
201+
return { status: 'redirect', popup }
202+
},
203+
}
204+
)
204205
}
205206

206207
interface DeleteMcpServerParams {

0 commit comments

Comments
 (0)