LiquidAI/LFM2-MCP
39
1import React, { useState } from "react";2import { discoverOAuthEndpoints, startOAuthFlow } from "../services/oauth";3import { Plus, Server, Wifi, WifiOff, Trash2, TestTube } from "lucide-react";4import { useMCP } from "../hooks/useMCP";5import type { MCPServerConfig } from "../types/mcp";6import { STORAGE_KEYS, DEFAULTS } from "../config/constants";7 8interface MCPServerManagerProps {9 isOpen: boolean;10 onClose: () => void;11}12 13export const MCPServerManager: React.FC<MCPServerManagerProps> = ({14 isOpen,15 onClose,16}) => {17 const {18 mcpState,19 addServer,20 removeServer,21 connectToServer,22 disconnectFromServer,23 testConnection,24 } = useMCP();25 const [showAddForm, setShowAddForm] = useState(false);26 const [testingConnection, setTestingConnection] = useState<string | null>(27 null28 );29 const [notification, setNotification] = useState<{30 message: string;31 type: "success" | "error";32 } | null>(null);33 34 const [newServer, setNewServer] = useState<Omit<MCPServerConfig, "id">>({35 name: "",36 url: "",37 enabled: true,38 transport: "streamable-http",39 auth: {40 type: "bearer",41 },42 });43 44 if (!isOpen) return null;45 46 const handleAddServer = async () => {47 if (!newServer.name || !newServer.url) return;48 49 const serverConfig: MCPServerConfig = {50 ...newServer,51 id: `server_${Date.now()}`,52 };53 54 // Persist name and transport for OAuth flow55 localStorage.setItem(STORAGE_KEYS.MCP_SERVER_NAME, newServer.name);56 localStorage.setItem(57 STORAGE_KEYS.MCP_SERVER_TRANSPORT,58 newServer.transport59 );60 61 try {62 await addServer(serverConfig);63 setNewServer({64 name: "",65 url: "",66 enabled: true,67 transport: "streamable-http",68 auth: {69 type: "bearer",70 },71 });72 setShowAddForm(false);73 } catch (error) {74 setNotification({75 message: `Failed to add server: ${76 error instanceof Error ? error.message : "Unknown error"77 }`,78 type: "error",79 });80 setTimeout(() => setNotification(null), DEFAULTS.OAUTH_ERROR_TIMEOUT);81 }82 };83 84 const handleTestConnection = async (config: MCPServerConfig) => {85 setTestingConnection(config.id);86 try {87 const success = await testConnection(config);88 if (success) {89 setNotification({90 message: "Connection test successful!",91 type: "success",92 });93 } else {94 setNotification({95 message: "Connection test failed. Please check your configuration.",96 type: "error",97 });98 }99 } catch (error) {100 setNotification({101 message: `Connection test failed: ${error}`,102 type: "error",103 });104 } finally {105 setTestingConnection(null);106 // Auto-hide notification after 3 seconds107 setTimeout(() => setNotification(null), DEFAULTS.NOTIFICATION_TIMEOUT);108 }109 };110 111 const handleToggleConnection = async (112 serverId: string,113 isConnected: boolean114 ) => {115 try {116 if (isConnected) {117 await disconnectFromServer(serverId);118 } else {119 await connectToServer(serverId);120 }121 } catch (error) {122 setNotification({123 message: `Failed to toggle connection: ${124 error instanceof Error ? error.message : "Unknown error"125 }`,126 type: "error",127 });128 setTimeout(() => setNotification(null), DEFAULTS.OAUTH_ERROR_TIMEOUT);129 }130 };131 132 return (133 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">134 <div className="bg-gray-800 rounded-lg p-6 w-full max-w-4xl max-h-[80vh] overflow-y-auto">135 <div className="flex justify-between items-center mb-6">136 <h2 className="text-2xl font-bold text-white flex items-center gap-2">137 <Server className="text-blue-400" />138 MCP Server Manager139 </h2>140 <button onClick={onClose} className="text-gray-400 hover:text-white">141 ✕142 </button>143 </div>144 145 {/* Add Server Button */}146 <div className="mb-6">147 <button148 onClick={() => setShowAddForm(!showAddForm)}149 className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"150 >151 <Plus size={16} />152 Add MCP Server153 </button>154 </div>155 156 {/* Add Server Form */}157 {showAddForm && (158 <div className="bg-gray-700 rounded-lg p-4 mb-6">159 <h3 className="text-lg font-semibold text-white mb-4">160 Add New MCP Server161 </h3>162 <div className="space-y-4">163 <div>164 <label className="block text-sm font-medium text-gray-300 mb-1">165 Server Name166 </label>167 <input168 type="text"169 value={newServer.name}170 onChange={(e) =>171 setNewServer({ ...newServer, name: e.target.value })172 }173 className="w-full bg-gray-600 text-white rounded px-3 py-2"174 placeholder="My MCP Server"175 />176 </div>177 178 <div>179 <label className="block text-sm font-medium text-gray-300 mb-1">180 Server URL181 </label>182 <input183 type="url"184 value={newServer.url}185 onChange={(e) =>186 setNewServer({ ...newServer, url: e.target.value })187 }188 className="w-full bg-gray-600 text-white rounded px-3 py-2"189 placeholder="http://localhost:3000/mcp"190 />191 </div>192 193 <div>194 <label className="block text-sm font-medium text-gray-300 mb-1">195 Transport196 </label>197 <select198 value={newServer.transport}199 onChange={(e) =>200 setNewServer({201 ...newServer,202 transport: e.target.value as MCPServerConfig["transport"],203 })204 }205 className="w-full bg-gray-600 text-white rounded px-3 py-2"206 >207 <option value="streamable-http">Streamable HTTP</option>208 <option value="sse">Server-Sent Events</option>209 </select>210 </div>211 212 <div>213 <label className="block text-sm font-medium text-gray-300 mb-1">214 Authentication215 </label>216 <select217 value={newServer.auth?.type || "none"}218 onChange={(e) => {219 const authType = e.target.value;220 if (authType === "none") {221 setNewServer({ ...newServer, auth: undefined });222 } else {223 setNewServer({224 ...newServer,225 auth: {226 type: authType as "bearer" | "basic" | "oauth",227 ...(authType === "bearer" ? { token: "" } : {}),228 ...(authType === "basic"229 ? { username: "", password: "" }230 : {}),231 ...(authType === "oauth" ? { token: "" } : {}),232 },233 });234 }235 }}236 className="w-full bg-gray-600 text-white rounded px-3 py-2"237 >238 <option value="none">No Authentication</option>239 <option value="bearer">Bearer Token</option>240 <option value="basic">Basic Auth</option>241 <option value="oauth">OAuth Token</option>242 </select>243 </div>244 245 {/* Auth-specific fields */}246 {newServer.auth?.type === "bearer" && (247 <div>248 <label className="block text-sm font-medium text-gray-300 mb-1">249 Bearer Token250 </label>251 <input252 type="password"253 value={newServer.auth.token || ""}254 onChange={(e) =>255 setNewServer({256 ...newServer,257 auth: { ...newServer.auth!, token: e.target.value },258 })259 }260 className="w-full bg-gray-600 text-white rounded px-3 py-2"261 placeholder="your-bearer-token"262 />263 </div>264 )}265 266 {newServer.auth?.type === "basic" && (267 <>268 <div>269 <label className="block text-sm font-medium text-gray-300 mb-1">270 Username271 </label>272 <input273 type="text"274 value={newServer.auth.username || ""}275 onChange={(e) =>276 setNewServer({277 ...newServer,278 auth: {279 ...newServer.auth!,280 username: e.target.value,281 },282 })283 }284 className="w-full bg-gray-600 text-white rounded px-3 py-2"285 placeholder="username"286 />287 </div>288 <div>289 <label className="block text-sm font-medium text-gray-300 mb-1">290 Password291 </label>292 <input293 type="password"294 value={newServer.auth.password || ""}295 onChange={(e) =>296 setNewServer({297 ...newServer,298 auth: {299 ...newServer.auth!,300 password: e.target.value,301 },302 })303 }304 className="w-full bg-gray-600 text-white rounded px-3 py-2"305 placeholder="password"306 />307 </div>308 </>309 )}310 311 {newServer.auth?.type === "oauth" && (312 <div>313 <label className="block text-sm font-medium text-gray-300 mb-1">314 OAuth Authorization315 </label>316 <button317 className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded mb-2"318 type="button"319 onClick={async () => {320 try {321 // Persist name and transport for OAuthCallback322 localStorage.setItem(323 STORAGE_KEYS.MCP_SERVER_NAME,324 newServer.name325 );326 localStorage.setItem(327 STORAGE_KEYS.MCP_SERVER_TRANSPORT,328 newServer.transport329 );330 const endpoints = await discoverOAuthEndpoints(331 newServer.url332 );333 334 if (!endpoints.clientId || !endpoints.redirectUri) {335 throw new Error(336 "Missing required OAuth configuration (clientId or redirectUri)"337 );338 }339 340 startOAuthFlow({341 authorizationEndpoint:342 endpoints.authorizationEndpoint,343 clientId: endpoints.clientId as string,344 redirectUri: endpoints.redirectUri as string,345 scopes: (endpoints.scopes || []) as string[],346 });347 } catch (err) {348 setNotification({349 message:350 "OAuth discovery failed: " +351 (err instanceof Error ? err.message : String(err)),352 type: "error",353 });354 setTimeout(355 () => setNotification(null),356 DEFAULTS.OAUTH_ERROR_TIMEOUT357 );358 }359 }}360 >361 Connect with OAuth362 </button>363 <p className="text-xs text-gray-400">364 You will be redirected to authorize this app with the MCP365 server.366 </p>367 </div>368 )}369 370 <div className="flex items-center gap-2">371 <input372 type="checkbox"373 id="enabled"374 checked={newServer.enabled}375 onChange={(e) =>376 setNewServer({ ...newServer, enabled: e.target.checked })377 }378 className="rounded"379 />380 <label htmlFor="enabled" className="text-sm text-gray-300">381 Auto-connect when added382 </label>383 </div>384 385 <div className="flex gap-2">386 <button387 onClick={handleAddServer}388 className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded"389 >390 Add Server391 </button>392 <button393 onClick={() => setShowAddForm(false)}394 className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded"395 >396 Cancel397 </button>398 </div>399 </div>400 </div>401 )}402 403 {/* Server List */}404 <div className="space-y-4">405 <h3 className="text-lg font-semibold text-white">406 Configured Servers407 </h3>408 409 {Object.values(mcpState.servers).length === 0 ? (410 <div className="text-gray-400 text-center py-8">411 No MCP servers configured. Add one to get started!412 </div>413 ) : (414 Object.values(mcpState.servers).map((connection) => (415 <div416 key={connection.config.id}417 className="bg-gray-700 rounded-lg p-4"418 >419 <div className="flex items-center justify-between">420 <div className="flex items-center gap-3">421 <div422 className={`w-3 h-3 rounded-full ${423 connection.isConnected ? "bg-green-400" : "bg-red-400"424 }`}425 />426 <div>427 <h4 className="text-white font-medium">428 {connection.config.name}429 </h4>430 <p className="text-gray-400 text-sm">431 {connection.config.url}432 </p>433 <p className="text-gray-500 text-xs">434 Transport: {connection.config.transport}435 {connection.config.auth &&436 ` • Auth: ${connection.config.auth.type}`}437 {connection.isConnected &&438 ` • ${connection.tools.length} tools available`}439 </p>440 </div>441 </div>442 443 <div className="flex items-center gap-2">444 {/* Test Connection */}445 <button446 onClick={() => handleTestConnection(connection.config)}447 disabled={testingConnection === connection.config.id}448 className="p-2 text-yellow-400 hover:text-yellow-300 disabled:opacity-50"449 title="Test Connection"450 >451 <TestTube size={16} />452 </button>453 454 {/* Connect/Disconnect */}455 <button456 onClick={() =>457 handleToggleConnection(458 connection.config.id,459 connection.isConnected460 )461 }462 className={`p-2 ${463 connection.isConnected464 ? "text-green-400 hover:text-green-300"465 : "text-gray-400 hover:text-gray-300"466 }`}467 title={connection.isConnected ? "Disconnect" : "Connect"}468 >469 {connection.isConnected ? (470 <Wifi size={16} />471 ) : (472 <WifiOff size={16} />473 )}474 </button>475 476 {/* Remove Server */}477 <button478 onClick={() => removeServer(connection.config.id)}479 className="p-2 text-red-400 hover:text-red-300"480 title="Remove Server"481 >482 <Trash2 size={16} />483 </button>484 </div>485 </div>486 487 {connection.lastError && (488 <div className="mt-2 text-red-400 text-sm">489 Error: {connection.lastError}490 </div>491 )}492 493 {connection.isConnected && connection.tools.length > 0 && (494 <div className="mt-3">495 <details className="text-sm">496 <summary className="text-gray-300 cursor-pointer">497 Available Tools ({connection.tools.length})498 </summary>499 <div className="mt-2 space-y-1">500 {connection.tools.map((tool) => (501 <div key={tool.name} className="text-gray-400 pl-4">502 • {tool.name} -{" "}503 {tool.description || "No description"}504 </div>505 ))}506 </div>507 </details>508 </div>509 )}510 </div>511 ))512 )}513 </div>514 515 {mcpState.error && (516 <div className="mt-4 p-4 bg-red-900 border border-red-700 rounded-lg text-red-200">517 <strong>Error:</strong> {mcpState.error}518 </div>519 )}520 521 {notification && (522 <div523 className={`mt-4 p-4 border rounded-lg ${524 notification.type === "success"525 ? "bg-green-900 border-green-700 text-green-200"526 : "bg-red-900 border-red-700 text-red-200"527 }`}528 >529 {notification.message}530 </div>531 )}532 </div>533 </div>534 );535};536 