diff --git a/demo/package.json b/demo/package.json index f46b0fc..76690bc 100644 --- a/demo/package.json +++ b/demo/package.json @@ -29,6 +29,7 @@ "rehype-highlight": "^7.0.2", "remark-gfm": "^4.0.1", "tailwindcss": "^4.0.14", + "use-mcp": "link:..", "workers-ai-provider": "^0.1.3" }, "devDependencies": { diff --git a/demo/pnpm-lock.yaml b/demo/pnpm-lock.yaml index 051cbc3..d7e5d41 100644 --- a/demo/pnpm-lock.yaml +++ b/demo/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: tailwindcss: specifier: ^4.0.14 version: 4.1.6 + use-mcp: + specifier: link:.. + version: link:.. workers-ai-provider: specifier: ^0.1.3 version: 0.1.3(react@19.1.0) diff --git a/demo/src/components/ChatSidebar.tsx b/demo/src/components/ChatSidebar.tsx index 9365ca0..cc31848 100644 --- a/demo/src/components/ChatSidebar.tsx +++ b/demo/src/components/ChatSidebar.tsx @@ -7,6 +7,7 @@ import { SquarePen, } from "lucide-react"; import ModelSelector from "./ModelSelector"; +import { McpServers } from "./McpServers"; import { type Model } from "../types/models"; import { type Message } from "../types"; @@ -155,13 +156,14 @@ const ChatSidebar: React.FC = ({ - {/* Model Selector at bottom */} -
+ {/* Model Selector and MCP Servers at bottom */} +
+
diff --git a/demo/src/components/McpServers.tsx b/demo/src/components/McpServers.tsx new file mode 100644 index 0000000..ddea4da --- /dev/null +++ b/demo/src/components/McpServers.tsx @@ -0,0 +1,341 @@ +import { useState, useRef, useEffect } from 'react' +import { useMcp, type Tool } from 'use-mcp/react' +import { Settings, Info } from 'lucide-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 +} + +export function McpServers({ + onToolsUpdate, +}: { + onToolsUpdate?: (tools: Tool[]) => void +}) { + 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]) + + // 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 + + ) + } + } + + // Log the tools to console when they change (for debugging) + useEffect(() => { + if (tools.length > 0) { + console.log('MCP Tools available:', tools) + } + }, [tools]) + + return ( +
+
+
+ MCP Servers +
+ +
+
+ +
+ +

+ Connect to Model Context Protocol (MCP) servers to access additional AI + capabilities. +

+ +
+
+ + {getStatusBadge()} +
+ + {error && ( +
+ {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 && ( + + )} +
+ ) +}