diff --git a/client/package.json b/client/package.json index 12cb70656f..a3f322301b 100644 --- a/client/package.json +++ b/client/package.json @@ -38,7 +38,7 @@ "@hyperdx/browser": "^0.24.0", "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", - "@mcp-ui/client": "^7.0.0", + "@modelcontextprotocol/ext-apps": "^1.7.4", "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "1.0.2", diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx index ce529c05fb..a713d60da2 100644 --- a/client/src/components/Chat/Messages/Content/ToolCall.tsx +++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx @@ -1,8 +1,7 @@ -import React, { useMemo, useState, useEffect, useCallback, useContext } from 'react'; +import React, { useRef, useMemo, useState, useEffect, useCallback } from 'react'; import { useRecoilValue } from 'recoil'; +import { Button } from '@librechat/client'; import { TriangleAlert } from 'lucide-react'; -import { AppRenderer } from '@mcp-ui/client'; -import { Button, ThemeContext, isDark } from '@librechat/client'; import { Constants, Tools, @@ -13,8 +12,8 @@ import { import type { TAttachment, UIResource } from 'librechat-data-provider'; import { useLocalize, useProgress, useExpandCollapse } from '~/hooks'; import { ToolIcon, getToolIconType, isError } from './ToolOutput'; -import { useMCPIconMap, useMCPAppCallbacks } from '~/hooks/MCP'; -import { getMCPSandboxConfig } from '~/utils/mcpApps'; +import { useMCPIconMap, useAppBridge } from '~/hooks/MCP'; +import { getMCPSandboxUrl } from '~/utils/mcpApps'; import { AttachmentGroup } from './Parts'; import ToolCallInfo from './ToolCallInfo'; import ProgressText from './ProgressText'; @@ -26,15 +25,14 @@ const SPINNER_TIMEOUT_MS = 10_000; const MCPAppView = React.memo(function MCPAppView({ app, args, - themeMode, }: { app: UIResource; args: string | Record; - themeMode: string; }) { + const iframeRef = useRef(null); const [height, setHeight] = useState(undefined); const [loaded, setLoaded] = useState(false); - const callbacks = useMCPAppCallbacks((app.serverName as string | undefined) ?? ''); + const sandboxUrl = useMemo(() => getMCPSandboxUrl(), []); useEffect(() => { if (loaded) return; @@ -42,13 +40,7 @@ const MCPAppView = React.memo(function MCPAppView({ return () => clearTimeout(timer); }, [loaded]); - const toolResult = useMemo(() => { - const sc = app.structuredContent as Record | undefined | null; - if (!sc || typeof sc !== 'object' || Array.isArray(sc)) return undefined; - return { content: [], structuredContent: sc }; - }, [app.structuredContent]); - - const toolInput = useMemo(() => { + const toolArgs = useMemo(() => { try { return typeof args === 'string' ? JSON.parse(args) : args; } catch { @@ -56,10 +48,11 @@ const MCPAppView = React.memo(function MCPAppView({ } }, [args]); - const hostContext = useMemo( - () => ({ theme: isDark(themeMode) ? ('dark' as const) : ('light' as const) }), - [themeMode], - ); + const toolResult = useMemo(() => { + const sc = app.structuredContent as Record | undefined | null; + if (!sc || typeof sc !== 'object' || Array.isArray(sc)) return undefined; + return { content: [] as [], structuredContent: sc }; + }, [app.structuredContent]); const handleSizeChanged = useCallback((params: { height?: number; width?: number }) => { if (params.height && params.height > 0) { @@ -68,7 +61,7 @@ const MCPAppView = React.memo(function MCPAppView({ } }, []); - const handleError = useCallback((err: Error) => logger.error('[MCP App]', err), []); + useAppBridge(iframeRef, app, toolArgs, toolResult, handleSizeChanged); return (
@@ -92,18 +85,17 @@ const MCPAppView = React.memo(function MCPAppView({ Loading interactive view...
)} - ); @@ -133,7 +125,6 @@ export default function ToolCall({ onExpand?: () => void; }) { const localize = useLocalize(); - const { theme: themeMode } = useContext(ThemeContext); const autoExpand = useRecoilValue(store.autoExpandTools); const hasOutput = (output?.length ?? 0) > 0; const [showInfo, setShowInfo] = useState(() => autoExpand && hasOutput); @@ -371,9 +362,7 @@ export default function ToolCall({ {!hideAttachments && attachments && attachments.length > 0 && ( )} - {mcpApp && hasOutput && ( - - )} + {mcpApp && hasOutput && } ); } diff --git a/client/src/components/Chat/Messages/Content/UIResourceCarousel.tsx b/client/src/components/Chat/Messages/Content/UIResourceCarousel.tsx index 4a9c56d4aa..2c168888c3 100644 --- a/client/src/components/Chat/Messages/Content/UIResourceCarousel.tsx +++ b/client/src/components/Chat/Messages/Content/UIResourceCarousel.tsx @@ -1,30 +1,52 @@ import React, { useState } from 'react'; -import { AppRenderer } from '@mcp-ui/client'; import type { UIResource } from 'librechat-data-provider'; -import { getMCPSandboxConfig } from '~/utils/mcpApps'; -import { useMCPAppCallbacks } from '~/hooks/MCP'; -import { logger } from '~/utils'; +import { getMCPSandboxUrl } from '~/utils/mcpApps'; +import { useAppBridge } from '~/hooks/MCP'; interface UIResourceCarouselProps { uiResources: UIResource[]; } function MCPAppCard({ resource }: { resource: UIResource }) { - const callbacks = useMCPAppCallbacks((resource.serverName as string | undefined) ?? ''); - const handleError = React.useCallback((err: Error) => logger.error('[MCP App]', err), []); + const iframeRef = React.useRef(null); + const [loaded, setLoaded] = useState(false); + const sandboxUrl = React.useMemo(() => getMCPSandboxUrl(), []); + + const toolResult = React.useMemo(() => { + const sc = resource.structuredContent as Record | undefined | null; + if (!sc || typeof sc !== 'object' || Array.isArray(sc)) return undefined; + return { content: [] as [], structuredContent: sc }; + }, [resource.structuredContent]); + + const handleSizeChanged = React.useCallback((params: { height?: number; width?: number }) => { + if (params.height && params.height > 0) { + setLoaded(true); + } + }, []); + + useAppBridge(iframeRef, resource, undefined, toolResult, handleSizeChanged); if (resource.toolName && resource.serverName) { return ( - + <> + {!loaded && ( +
+ Loading interactive view... +
+ )} +