diff --git a/web/app/components/tools/mcp/detail/content.tsx b/web/app/components/tools/mcp/detail/content.tsx index c3df795836..60f2c13b5f 100644 --- a/web/app/components/tools/mcp/detail/content.tsx +++ b/web/app/components/tools/mcp/detail/content.tsx @@ -1,6 +1,5 @@ 'use client' -import React, { useCallback } from 'react' -import { useRouter } from 'next/navigation' +import React, { useCallback, useEffect } from 'react' import type { FC } from 'react' import { useBoolean } from 'ahooks' import { useTranslation } from 'react-i18next' @@ -26,29 +25,35 @@ import { useInvalidateMCPTools, useMCPTools, useUpdateMCP, + useUpdateMCPAuthorizationToken, useUpdateMCPTools, } from '@/service/use-tools' +import { openOAuthPopup } from '@/hooks/use-oauth' import cn from '@/utils/classnames' type Props = { detail: ToolWithProvider onUpdate: (isDelete?: boolean) => void onHide: () => void + isCreation: boolean + onFirstCreate: () => void } const MCPDetailContent: FC = ({ detail, onUpdate, onHide, + isCreation, + onFirstCreate, }) => { const { t } = useTranslation() - const router = useRouter() const { isCurrentWorkspaceManager } = useAppContext() const { data, isFetching: isGettingTools } = useMCPTools(detail.is_team_authorization ? detail.id : '') const invalidateMCPTools = useInvalidateMCPTools() const { mutateAsync: updateTools, isPending: isUpdating } = useUpdateMCPTools() const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useAuthorizeMCP() + const { mutateAsync: updateMCPAuthorizationToken } = useUpdateMCPAuthorizationToken() const toolList = data?.tools || [] const handleUpdateTools = useCallback(async () => { @@ -81,7 +86,22 @@ const MCPDetailContent: FC = ({ setFalse: hideDeleting, }] = useBoolean(false) + const handleOAuthCallback = async (state: string, code: string) => { + if (!isCurrentWorkspaceManager) + return + if (detail.id !== state) + return + await updateMCPAuthorizationToken({ + provider_id: state, + authorization_code: code, + }) + handleUpdateTools() + } + const handleAuthorize = useCallback(async () => { + onFirstCreate() + if (!isCurrentWorkspaceManager) + return if (!detail) return const res = await authorizeMcp({ @@ -91,7 +111,7 @@ const MCPDetailContent: FC = ({ handleUpdateTools() else if (res.authorization_url) - router.push(res.authorization_url) + openOAuthPopup(res.authorization_url, handleOAuthCallback) }, [detail, updateMCP, hideUpdateModal, onUpdate]) const handleUpdate = useCallback(async (data: any) => { @@ -119,6 +139,11 @@ const MCPDetailContent: FC = ({ } }, [detail, showDeleting, hideDeleting, hideDeleteConfirm, onUpdate]) + useEffect(() => { + if (isCreation) + handleAuthorize() + }, []) + if (!detail) return null diff --git a/web/app/components/tools/mcp/detail/provider-detail.tsx b/web/app/components/tools/mcp/detail/provider-detail.tsx index effb2363c9..1ac4223fa4 100644 --- a/web/app/components/tools/mcp/detail/provider-detail.tsx +++ b/web/app/components/tools/mcp/detail/provider-detail.tsx @@ -10,12 +10,16 @@ type Props = { detail?: ToolWithProvider onUpdate: () => void onHide: () => void + isCreation: boolean + onFirstCreate: () => void } const MCPDetailPanel: FC = ({ detail, onUpdate, onHide, + isCreation, + onFirstCreate, }) => { const handleUpdate = (isDelete = false) => { if (isDelete) @@ -41,6 +45,8 @@ const MCPDetailPanel: FC = ({ detail={detail} onHide={onHide} onUpdate={handleUpdate} + isCreation={isCreation} + onFirstCreate={onFirstCreate} /> )} diff --git a/web/app/components/tools/mcp/index.tsx b/web/app/components/tools/mcp/index.tsx index fa069ac7f0..b6ed308d5b 100644 --- a/web/app/components/tools/mcp/index.tsx +++ b/web/app/components/tools/mcp/index.tsx @@ -1,15 +1,10 @@ 'use client' -import { useEffect, useMemo, useState } from 'react' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { useMemo, useState } from 'react' import NewMCPCard from './create-card' import MCPCard from './provider-card' import MCPDetailPanel from './detail/provider-detail' import { useAllMCPTools, - useAuthorizeMCP, - useInvalidateMCPTools, - useUpdateMCPAuthorizationToken, - useUpdateMCPTools, } from '@/service/use-tools' import type { ToolWithProvider } from '@/app/components/workflow/types' import cn from '@/utils/classnames' @@ -39,17 +34,8 @@ function renderDefaultCard() { const MCPList = ({ searchText, }: Props) => { - const router = useRouter() - const pathname = usePathname() - const searchParams = useSearchParams() - const authCode = searchParams.get('code') || '' - const providerID = searchParams.get('state') || '' - const { data: list = [], refetch } = useAllMCPTools() - const { mutateAsync: authorizeMcp } = useAuthorizeMCP() - const { mutateAsync: updateTools } = useUpdateMCPTools() - const invalidateMCPTools = useInvalidateMCPTools() - const { mutateAsync: updateMCPAuthorizationToken } = useUpdateMCPAuthorizationToken() + const [isCreation, setIsCreation] = useState(false) const filteredList = useMemo(() => { return list.filter((collection) => { @@ -68,40 +54,9 @@ const MCPList = ({ const handleCreate = async (provider: ToolWithProvider) => { await refetch() // update list setCurrentProviderID(provider.id) - const res = await authorizeMcp({ - provider_id: provider.id, - }) - if (res.result === 'success') { - await refetch() // update authorization in list - await updateTools(provider.id) - invalidateMCPTools(provider.id) - await refetch() // update tool list in provider list - } - else if (res.authorization_url) { - router.push(res.authorization_url) - } + setIsCreation(true) } - const handleUpdateAuthorization = async (providerID: string, code: string) => { - const targetProvider = list.find(provider => provider.id === providerID) - router.replace(pathname) - if (!targetProvider) return - await updateMCPAuthorizationToken({ - provider_id: providerID, - authorization_code: code, - }) - await refetch() - setCurrentProviderID(providerID) - await updateTools(providerID) - invalidateMCPTools(providerID) - await refetch() - } - - useEffect(() => { - if (authCode && providerID && list.length > 0) - handleUpdateAuthorization(providerID, authCode) - }, [authCode, providerID, list]) - return ( <>
setCurrentProviderID(undefined)} onUpdate={refetch} + isCreation={isCreation} + onFirstCreate={() => setIsCreation(false)} /> )} diff --git a/web/app/components/tools/provider-list.tsx b/web/app/components/tools/provider-list.tsx index 8aee0beb0e..35a3ab3dc7 100644 --- a/web/app/components/tools/provider-list.tsx +++ b/web/app/components/tools/provider-list.tsx @@ -20,11 +20,13 @@ import MCPList from './mcp' import { useSelector as useAppContextSelector } from '@/context/app-context' import { useAllToolProviders } from '@/service/use-tools' import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' +import { useOAuthCallback } from '@/hooks/use-oauth' const ProviderList = () => { const { t } = useTranslation() const containerRef = useRef(null) const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + useOAuthCallback() const searchParams = useSearchParams() const authCode = searchParams.get('code') || '' diff --git a/web/hooks/use-oauth.ts b/web/hooks/use-oauth.ts new file mode 100644 index 0000000000..0fe4d8e926 --- /dev/null +++ b/web/hooks/use-oauth.ts @@ -0,0 +1,47 @@ +'use client' +import { useEffect } from 'react' +import { useSearchParams } from 'next/navigation' + +export const useOAuthCallback = () => { + const searchParams = useSearchParams() + + useEffect(() => { + const code = searchParams.get('code') + const state = searchParams.get('state') + + if (code && state && window.opener) { + window.opener.postMessage({ + type: 'oauth_callback', + payload: { + code, + state, + }, + }, '*') + window.close() + } + }, [searchParams]) +} + +export const openOAuthPopup = (url: string, callback: (state: string, code: string) => void) => { + const width = 600 + const height = 600 + const left = window.screenX + (window.outerWidth - width) / 2 + const top = window.screenY + (window.outerHeight - height) / 2 + + const popup = window.open( + url, + 'OAuth', + `width=${width},height=${height},left=${left},top=${top},scrollbars=yes`, + ) + + const handleMessage = (event: MessageEvent) => { + if (event.data?.type === 'oauth_callback') { + window.removeEventListener('message', handleMessage) + const { code, state } = event.data.payload + callback(state, code) + } + } + + window.addEventListener('message', handleMessage) + return popup +}