diff --git a/apps/mesh/src/web/components/chat/no-llm-binding-empty-state.tsx b/apps/mesh/src/web/components/chat/no-llm-binding-empty-state.tsx index 8b2a8ab31d..46b6ac08f1 100644 --- a/apps/mesh/src/web/components/chat/no-llm-binding-empty-state.tsx +++ b/apps/mesh/src/web/components/chat/no-llm-binding-empty-state.tsx @@ -1,4 +1,5 @@ import { useNavigate } from "@tanstack/react-router"; +import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { Button } from "@deco/ui/components/button.tsx"; import { EmptyState } from "../empty-state"; @@ -11,7 +12,7 @@ import { } from "@decocms/mesh-sdk"; import { generatePrefixedId } from "@/shared/utils/generate-id"; import { authClient } from "@/web/lib/auth-client"; -import { useDecoChatOpen } from "@/web/hooks/use-deco-chat-open"; +import { authenticateConnection } from "@/web/lib/authenticate-connection"; interface NoLlmBindingEmptyStateProps { title?: string; @@ -29,8 +30,8 @@ export function NoLlmBindingEmptyState({ org, }: NoLlmBindingEmptyStateProps) { const actions = useConnectionActions(); - const [, setDecoChatOpen] = useDecoChatOpen(); const navigate = useNavigate(); + const queryClient = useQueryClient(); const { data: session } = authClient.useSession(); const allConnections = useConnections(); @@ -56,35 +57,36 @@ export function NoLlmBindingEmptyState({ (conn) => conn.connection_url === OPENROUTER_MCP_URL, ); - if (existingConnection) { - setDecoChatOpen(false); - navigate({ - to: "/$org/$project/mcps/$connectionId", - params: { - org: org.slug, - project: ORG_ADMIN_PROJECT_SLUG, - connectionId: existingConnection.id, - }, - }); - return; - } + const connectionId = existingConnection?.id ?? null; - // Create new OpenRouter connection - const connectionData = getWellKnownOpenRouterConnection({ - id: generatePrefixedId("conn"), - }); + if (!connectionId) { + // Create new OpenRouter connection + const connectionData = getWellKnownOpenRouterConnection({ + id: generatePrefixedId("conn"), + }); - const result = await actions.create.mutateAsync(connectionData); + const result = await actions.create.mutateAsync(connectionData); - setDecoChatOpen(false); - navigate({ - to: "/$org/$project/mcps/$connectionId", - params: { - org: org.slug, - project: ORG_ADMIN_PROJECT_SLUG, - connectionId: result.id, - }, - }); + // Immediately trigger OAuth auth — opens popup, stays on Home + const success = await authenticateConnection( + result.id, + actions, + queryClient, + ); + if (success) { + toast.success("OpenRouter connected successfully"); + } + } else { + // Connection exists but may not be authenticated — trigger auth + const success = await authenticateConnection( + connectionId, + actions, + queryClient, + ); + if (success) { + toast.success("OpenRouter authenticated successfully"); + } + } } catch (error) { toast.error( `Failed to connect OpenRouter: ${error instanceof Error ? error.message : String(error)}`, diff --git a/apps/mesh/src/web/components/details/connection/index.tsx b/apps/mesh/src/web/components/details/connection/index.tsx index 4023c313ec..e4becdf6be 100644 --- a/apps/mesh/src/web/components/details/connection/index.tsx +++ b/apps/mesh/src/web/components/details/connection/index.tsx @@ -12,7 +12,7 @@ import { useCollectionBindings, } from "@/web/hooks/use-binding"; import { useMCPAuthStatus } from "@/web/hooks/use-mcp-auth-status"; -import { authenticateMcp } from "@/web/lib/mcp-oauth"; +import { authenticateConnection } from "@/web/lib/authenticate-connection"; import { KEYS } from "@/web/lib/query-keys"; import { Breadcrumb, @@ -338,75 +338,14 @@ function ConnectionInspectorViewWithConnection({ }; const handleAuthenticate = async () => { - const { token, tokenInfo, error } = await authenticateMcp({ - connectionId: connection.id, - }); - if (error || !token) { - toast.error(`Authentication failed: ${error}`); - return; - } - - if (tokenInfo) { - try { - const response = await fetch( - `/api/connections/${connection.id}/oauth-token`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - body: JSON.stringify({ - accessToken: tokenInfo.accessToken, - refreshToken: tokenInfo.refreshToken, - expiresIn: tokenInfo.expiresIn, - scope: tokenInfo.scope, - clientId: tokenInfo.clientId, - clientSecret: tokenInfo.clientSecret, - tokenEndpoint: tokenInfo.tokenEndpoint, - }), - }, - ); - if (!response.ok) { - console.error("Failed to save OAuth token:", await response.text()); - await connectionActions.update.mutateAsync({ - id: connection.id, - data: { connection_token: token }, - }); - } else { - try { - await connectionActions.update.mutateAsync({ - id: connection.id, - data: {}, - }); - } catch (err) { - console.warn( - "Failed to refresh connection tools after OAuth:", - err, - ); - } - } - } catch (err) { - console.error("Error saving OAuth token:", err); - await connectionActions.update.mutateAsync({ - id: connection.id, - data: { connection_token: token }, - }); - } - } else { - await connectionActions.update.mutateAsync({ - id: connection.id, - data: { connection_token: token }, - }); - } - - const mcpProxyUrl = new URL( - `/mcp/${connection.id}`, - window.location.origin, + const success = await authenticateConnection( + connection.id, + connectionActions, + queryClient, ); - await queryClient.invalidateQueries({ - queryKey: KEYS.isMCPAuthenticated(mcpProxyUrl.href, null), - }); - - toast.success("Authentication successful"); + if (success) { + toast.success("Authentication successful"); + } }; const handleRemoveOAuth = async () => { diff --git a/apps/mesh/src/web/lib/authenticate-connection.ts b/apps/mesh/src/web/lib/authenticate-connection.ts new file mode 100644 index 0000000000..5e9dae19bf --- /dev/null +++ b/apps/mesh/src/web/lib/authenticate-connection.ts @@ -0,0 +1,82 @@ +import { authenticateMcp } from "@/web/lib/mcp-oauth"; +import { KEYS } from "@/web/lib/query-keys"; +import type { QueryClient } from "@tanstack/react-query"; +import type { useConnectionActions } from "@decocms/mesh-sdk"; +import { toast } from "sonner"; + +/** + * Runs the full OAuth authentication flow for an MCP connection: + * 1. Opens the OAuth popup via authenticateMcp() + * 2. Saves the token via the OAuth endpoint (or falls back to connection_token) + * 3. Invalidates auth-related queries so the UI refreshes + * + * Returns true on success, false on failure. + */ +export async function authenticateConnection( + connectionId: string, + connectionActions: ReturnType, + queryClient: QueryClient, +): Promise { + const { token, tokenInfo, error } = await authenticateMcp({ connectionId }); + + if (error || !token) { + toast.error(`Authentication failed: ${error}`); + return false; + } + + if (tokenInfo) { + try { + const response = await fetch( + `/api/connections/${connectionId}/oauth-token`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ + accessToken: tokenInfo.accessToken, + refreshToken: tokenInfo.refreshToken, + expiresIn: tokenInfo.expiresIn, + scope: tokenInfo.scope, + clientId: tokenInfo.clientId, + clientSecret: tokenInfo.clientSecret, + tokenEndpoint: tokenInfo.tokenEndpoint, + }), + }, + ); + if (!response.ok) { + console.error("Failed to save OAuth token:", await response.text()); + await connectionActions.update.mutateAsync({ + id: connectionId, + data: { connection_token: token }, + }); + } else { + try { + await connectionActions.update.mutateAsync({ + id: connectionId, + data: {}, + }); + } catch (err) { + console.warn("Failed to refresh connection tools after OAuth:", err); + } + } + } catch (err) { + console.error("Error saving OAuth token:", err); + await connectionActions.update.mutateAsync({ + id: connectionId, + data: { connection_token: token }, + }); + } + } else { + await connectionActions.update.mutateAsync({ + id: connectionId, + data: { connection_token: token }, + }); + } + + const mcpProxyUrl = new URL(`/mcp/${connectionId}`, window.location.origin); + await queryClient.invalidateQueries({ + queryKey: KEYS.isMCPAuthenticated(mcpProxyUrl.href, null), + }); + + return true; +}