import { UIResourceRenderer, UIActionResultIntent, UIActionResultLink, UIActionResultNotification, UIActionResultPrompt, UIActionResultToolCall, UIActionResult, } from '@mcp-ui/client'; import { useState, useEffect } from 'react'; import { toast } from 'react-toastify'; import { EmbeddedResource } from '../api'; import { useTheme } from '../contexts/ThemeContext'; interface MCPUIResourceRendererProps { content: EmbeddedResource & { type: 'resource' }; appendPromptToChat?: (value: string) => void; } // More specific result types using discriminated unions type UIActionHandlerSuccess = { status: 'success'; data?: T; message?: string; }; type UIActionHandlerError = { status: 'error'; error: { code: UIActionErrorCode; message: string; details?: unknown; }; }; type UIActionHandlerPending = { status: 'pending'; message: string; }; type UIActionHandlerResult = | UIActionHandlerSuccess | UIActionHandlerError | UIActionHandlerPending; // Strongly typed error codes enum UIActionErrorCode { UNSUPPORTED_ACTION = 'UNSUPPORTED_ACTION', UNKNOWN_ACTION = 'UNKNOWN_ACTION', TOOL_NOT_FOUND = 'TOOL_NOT_FOUND', TOOL_EXECUTION_FAILED = 'TOOL_EXECUTION_FAILED', NAVIGATION_FAILED = 'NAVIGATION_FAILED', PROMPT_FAILED = 'PROMPT_FAILED', INTENT_FAILED = 'INTENT_FAILED', INVALID_PARAMS = 'INVALID_PARAMS', NETWORK_ERROR = 'NETWORK_ERROR', TIMEOUT = 'TIMEOUT', } // toast component const ToastComponent = ({ messageType, message, isImplemented = true, }: { messageType: string; message?: string; isImplemented?: boolean; }) => { const title = `MCP-UI ${messageType} message`; return (

{title}

{isImplemented ? (

Message received for {message}.

) : (

Message received for {message}.
{messageType.charAt(0).toUpperCase() + messageType.slice(1)} messages aren't supported yet, refer to console for more details.

)}
); }; export default function MCPUIResourceRenderer({ content, appendPromptToChat, }: MCPUIResourceRendererProps) { const { resolvedTheme } = useTheme(); const [proxyUrl, setProxyUrl] = useState(undefined); useEffect(() => { const fetchProxyUrl = async () => { try { const gooseApiHost = await window.electron.getGoosedHostPort(); const secretKey = await window.electron.getSecretKey(); if (gooseApiHost && secretKey) { setProxyUrl(`${gooseApiHost}/mcp-ui-proxy?secret=${encodeURIComponent(secretKey)}`); } else { console.error('Failed to get goosed host/port or secret key'); } } catch (error) { console.error('Error fetching MCP-UI Proxy URL:', error); } }; fetchProxyUrl().catch(console.error); }, []); const handleUIAction = async (actionEvent: UIActionResult): Promise => { // result to pass back to the MCP-UI let result: UIActionHandlerResult; const handleToolAction = async ( actionEvent: UIActionResultToolCall ): Promise => { const { toolName, params } = actionEvent.payload; toast.info(, { theme: resolvedTheme, }); return { status: 'error' as const, error: { code: UIActionErrorCode.UNSUPPORTED_ACTION, message: 'Tool calls are not yet implemented', details: { toolName, params }, }, }; }; const handlePromptAction = async ( actionEvent: UIActionResultPrompt ): Promise => { const { prompt } = actionEvent.payload; if (appendPromptToChat) { try { appendPromptToChat(prompt); window.dispatchEvent(new CustomEvent('scroll-chat-to-bottom')); return { status: 'success' as const, message: 'Prompt sent to chat successfully', }; } catch (error) { return { status: 'error' as const, error: { code: UIActionErrorCode.PROMPT_FAILED, message: 'Failed to send prompt to chat', details: error instanceof Error ? error.message : error, }, }; } } return { status: 'error' as const, error: { code: UIActionErrorCode.UNSUPPORTED_ACTION, message: 'Prompt handling is not implemented - append prop is required', details: { prompt }, }, }; }; const handleLinkAction = async ( actionEvent: UIActionResultLink ): Promise => { const { url } = actionEvent.payload; try { const urlObj = new URL(url); if (!['http:', 'https:'].includes(urlObj.protocol)) { return { status: 'error' as const, error: { code: UIActionErrorCode.NAVIGATION_FAILED, message: `Blocked potentially unsafe URL protocol: ${urlObj.protocol}`, details: { url, protocol: urlObj.protocol }, }, }; } await window.electron.openExternal(url); return { status: 'success' as const, message: `Opened ${url} in default browser`, }; } catch (error) { if (error instanceof TypeError && error.message.includes('Invalid URL')) { return { status: 'error' as const, error: { code: UIActionErrorCode.INVALID_PARAMS, message: `Invalid URL format: ${url}`, details: { url, error: error.message }, }, }; } else if (error instanceof Error && error.message.includes('Failed to open')) { return { status: 'error' as const, error: { code: UIActionErrorCode.NAVIGATION_FAILED, message: `Failed to open URL in default browser`, details: { url, error: error.message }, }, }; } else { return { status: 'error' as const, error: { code: UIActionErrorCode.NAVIGATION_FAILED, message: `Unexpected error opening URL: ${url}`, details: error instanceof Error ? error.message : error, }, }; } } }; const handleNotifyAction = async ( actionEvent: UIActionResultNotification ): Promise => { const { message } = actionEvent.payload; toast.info(, { theme: resolvedTheme, }); return { status: 'success' as const, data: { displayedAt: new Date().toISOString(), message: 'Notification displayed', details: actionEvent.payload, }, }; }; const handleIntentAction = async ( actionEvent: UIActionResultIntent ): Promise => { toast.info( , { theme: resolvedTheme, } ); return { status: 'error' as const, error: { code: UIActionErrorCode.UNSUPPORTED_ACTION, message: 'Intent handling is not yet implemented', details: actionEvent.payload, }, }; }; try { switch (actionEvent.type) { case 'tool': result = await handleToolAction(actionEvent); break; case 'prompt': result = await handlePromptAction(actionEvent); break; case 'link': result = await handleLinkAction(actionEvent); break; case 'notify': result = await handleNotifyAction(actionEvent); break; case 'intent': result = await handleIntentAction(actionEvent); break; default: { const _exhaustiveCheck: never = actionEvent; console.error('Unhandled MCP-UI action type:', _exhaustiveCheck); result = { status: 'error', error: { code: UIActionErrorCode.UNKNOWN_ACTION, message: `Unknown action type`, details: actionEvent, }, }; } } } catch (error) { console.error('Unexpected error handling MCP-UI action:', error); result = { status: 'error', error: { code: UIActionErrorCode.UNKNOWN_ACTION, message: 'An unexpected error occurred', details: error instanceof Error ? error.stack : error, }, }; } return result; }; return (
); }