CoolFace
Apppublic

LiquidAI/LFM2-MCP

sourceHugging Faceapache-2.0updated 3mo agoView on Hugging Face
39likes
MCPServerManager.tsx536 linesDownload Raw Back to components
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">141142          </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