From 11360f38fc0fd9c815ed5add91fd523dc3c67279 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Mon, 23 Jun 2025 16:02:05 +1000 Subject: [PATCH] Add MCP server indicator with plug emoji and modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create McpServerModal component based on existing McpServers component - Add plug emoji (🔌) indicator on right side, opposite to brain emoji - Show '0' when no servers connected, '1 (N)' when connected with N tools - Fullscreen modal with MCP server configuration UI - Support for single MCP server connection (foundation for multiple servers) - Integrate with existing MCP tools logic and useMcp hook - Clean layout with brain emoji on left, plug emoji on right --- .../src/components/ConversationThread.tsx | 24 +- .../chat-ui/src/components/McpServerModal.tsx | 367 ++++++++++++++++++ 2 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 examples/chat-ui/src/components/McpServerModal.tsx diff --git a/examples/chat-ui/src/components/ConversationThread.tsx b/examples/chat-ui/src/components/ConversationThread.tsx index 5ef2c42..419e265 100644 --- a/examples/chat-ui/src/components/ConversationThread.tsx +++ b/examples/chat-ui/src/components/ConversationThread.tsx @@ -9,6 +9,7 @@ import { type Tool } from 'use-mcp/react' import ChatMessage from './ChatMessage' import ChatInput from './ChatInput' import ModelSelectionModal from './ModelSelectionModal' +import McpServerModal from './McpServerModal' import { useAutoscroll } from '../hooks/useAutoscroll' import { useStreamResponse } from '../hooks/useStreamResponse' import { setApiKey } from '../utils/apiKeys' @@ -46,6 +47,7 @@ const ConversationThread: React.FC = ({ model: null, }) const [modelSelectionModal, setModelSelectionModal] = useState(false) + const [mcpServerModal, setMcpServerModal] = useState(false) const { messagesEndRef, messagesContainerRef, scrollToBottom } = useAutoscroll() @@ -220,8 +222,8 @@ const ConversationThread: React.FC = ({ messagesCount={currentConversation.messages.length} /> - {/* Model selector indicator */} -
+ {/* Model selector and MCP server indicators */} +
+ +
@@ -253,6 +265,14 @@ const ConversationThread: React.FC = ({ onModelChange={onModelChange} apiKeyUpdateTrigger={apiKeyUpdateTrigger} /> + + setMcpServerModal(false)} + onToolsUpdate={(tools) => { + // This will be handled by the existing MCP tools logic in ChatApp + }} + /> ) } diff --git a/examples/chat-ui/src/components/McpServerModal.tsx b/examples/chat-ui/src/components/McpServerModal.tsx new file mode 100644 index 0000000..fbfbca7 --- /dev/null +++ b/examples/chat-ui/src/components/McpServerModal.tsx @@ -0,0 +1,367 @@ +import React, { useState, useRef, useEffect } from 'react' +import { X, Info, Settings } from 'lucide-react' +import { useMcp, type Tool } from 'use-mcp/react' + +// MCP Connection wrapper that only renders when active +function McpConnection({ + serverUrl, + onConnectionUpdate, +}: { + serverUrl: string + onConnectionUpdate: (data: any) => void +}) { + // Use the MCP hook with the server URL + const connection = useMcp({ + url: serverUrl, + debug: true, + autoRetry: false, + popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes', + }) + + // Update parent component with connection data + useEffect(() => { + onConnectionUpdate(connection) + }, [ + connection.state, + connection.tools, + connection.error, + connection.log.length, + connection.authUrl, + ]) + + // Return null as this is just a hook wrapper + return null +} + +interface McpServerModalProps { + isOpen: boolean + onClose: () => void + onToolsUpdate?: (tools: Tool[]) => void +} + +const McpServerModal: React.FC = ({ + isOpen, + onClose, + onToolsUpdate, +}) => { + const [serverUrl, setServerUrl] = useState(() => { + return sessionStorage.getItem('mcpServerUrl') || '' + }) + const [isActive, setIsActive] = useState(false) + const [showSettings, setShowSettings] = useState(false) + const [connectionData, setConnectionData] = useState({ + state: 'not-connected', + tools: [], + error: undefined, + log: [], + authUrl: undefined, + retry: () => {}, + disconnect: () => {}, + authenticate: () => Promise.resolve(undefined), + callTool: (_name: string, _args?: Record) => + Promise.resolve(undefined), + clearStorage: () => {}, + }) + const logRef = useRef(null) + + // Extract connection properties + const { state, tools, error, log, authUrl, disconnect, authenticate } = + connectionData + + // Notify parent component when tools change + useEffect(() => { + if (onToolsUpdate && tools.length > 0) { + onToolsUpdate( + tools.map((t: Tool) => ({ + ...t, + callTool: (args: Record) => + connectionData.callTool(t.name, args), + })), + ) + } + }, [tools, onToolsUpdate]) + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden' + } else { + document.body.style.overflow = 'unset' + } + + return () => { + document.body.style.overflow = 'unset' + } + }, [isOpen]) + + // Handle connection + const handleConnect = () => { + if (!serverUrl.trim()) return + setIsActive(true) + } + + // Handle disconnection + const handleDisconnect = () => { + disconnect() + setIsActive(false) + setConnectionData({ + state: 'not-connected', + tools: [], + error: undefined, + log: [], + authUrl: undefined, + retry: () => {}, + disconnect: () => {}, + authenticate: () => Promise.resolve(undefined), + callTool: (_name: string, _args?: Record) => + Promise.resolve(undefined), + clearStorage: () => {}, + }) + } + + // Handle authentication if popup was blocked + const handleManualAuth = async () => { + try { + await authenticate() + } catch (err) { + console.error('Authentication error:', err) + } + } + + // Auto-scroll log to bottom + useEffect(() => { + if (logRef.current) { + logRef.current.scrollTop = logRef.current.scrollHeight + } + }, [log]) + + // Generate status badge based on connection state + const getStatusBadge = () => { + const baseClasses = 'px-2 py-1 rounded-full text-xs font-medium' + + switch (state) { + case 'discovering': + return ( + + Discovering + + ) + case 'authenticating': + return ( + + Authenticating + + ) + case 'connecting': + return ( + + Connecting + + ) + case 'loading': + return ( + + Loading + + ) + case 'ready': + return ( + + Connected + + ) + case 'failed': + return ( + + Failed + + ) + case 'not-connected': + default: + return ( + + Not Connected + + ) + } + } + + if (!isOpen) return null + + return ( + <> +
+
+
+

MCP Servers

+ +
+ +
+
+
+ + + Connect to Model Context Protocol (MCP) servers to access additional AI capabilities. + +
+ +
+ +
+
+ + {getStatusBadge()} +
+ + {error && state === 'failed' && ( +
+ {error} +
+ )} + +
+ { + const newValue = e.target.value + setServerUrl(newValue) + sessionStorage.setItem('mcpServerUrl', newValue) + }} + disabled={isActive && state !== 'failed'} + /> + + {state === 'ready' || + (isActive && state !== 'not-connected' && state !== 'failed') ? ( + + ) : ( + + )} +
+ + {/* Authentication Link if needed */} + {authUrl && ( +
+

+ Authentication required. Please click the link below: +

+ + Authenticate in new window + +
+ )} + + {/* Tools display when connected */} + {state === 'ready' && tools.length > 0 && ( +
+

+ Available Tools ({tools.length}) +

+
+ {tools.map((tool: Tool, index: number) => ( +
+ {tool.name} + {tool.description && ( +

+ {tool.description} +

+ )} +
+ ))} +
+
+ )} + + {/* Debug Log */} + {showSettings && ( +
+ +
+ {log.length > 0 ? ( + log.map((entry: any, index: number) => ( +
+ [{entry.level}] {entry.message} +
+ )) + ) : ( +
No log entries yet
+ )} +
+ {connectionData?.state !== 'not-connected' && ( + + )} +
+ )} +
+
+
+
+ + {/* Only render the actual MCP connection when active */} + {isActive && ( + + )} + + ) +} + +export default McpServerModal