/* ┌──────────────────────────────────────────────────────────────────────────────┐ │ @author: Davidson Gomes │ │ @file: /app/mcp-servers/page.tsx │ │ Developed by: Davidson Gomes │ │ Creation date: May 13, 2025 │ │ Contact: contato@evolution-api.com │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @copyright © Evolution API 2025. All rights reserved. │ │ Licensed under the Apache License, Version 2.0 │ │ │ │ You may not use this file except in compliance with the License. │ │ You may obtain a copy of the License at │ │ │ │ http://www.apache.org/licenses/LICENSE-2.0 │ │ │ │ Unless required by applicable law or agreed to in writing, software │ │ distributed under the License is distributed on an "AS IS" BASIS, │ │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ │ See the License for the specific language governing permissions and │ │ limitations under the License. │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @important │ │ For any future changes to the code in this file, it is recommended to │ │ include, together with the modification, the information of the developer │ │ who changed it and the date of modification. │ └──────────────────────────────────────────────────────────────────────────────┘ */ "use client" import type React from "react" import { useState, useEffect } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { useToast } from "@/components/ui/use-toast" import { Plus, MoreHorizontal, Edit, Trash2, Search, PenToolIcon as Tool } from "lucide-react" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { createMCPServer, listMCPServers, getMCPServer, updateMCPServer, deleteMCPServer, } from "@/services/mcpServerService" import { MCPServer, MCPServerCreate, ToolConfig } from "@/types/mcpServer" export default function MCPServersPage() { const { toast } = useToast() const [isLoading, setIsLoading] = useState(false) const [searchQuery, setSearchQuery] = useState("") const [isDialogOpen, setIsDialogOpen] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [selectedServer, setSelectedServer] = useState(null) const [activeTab, setActiveTab] = useState("basic") const [serverData, setServerData] = useState<{ name: string description: string type: string config_type: "sse" | "studio" url: string headers: { key: string; value: string }[] command: string args: string environments: { key: string }[] tools: ToolConfig[] }>({ name: "", description: "", type: "official", config_type: "sse", url: "", headers: [{ key: "x-api-key", value: "" }], command: "npx", args: "", environments: [], tools: [], }) const [page, setPage] = useState(1) const [limit, setLimit] = useState(10) const [total, setTotal] = useState(0) const [mcpServers, setMcpServers] = useState([]) useEffect(() => { const fetchServers = async () => { setIsLoading(true) try { const res = await listMCPServers((page - 1) * limit, limit) setMcpServers(res.data) setTotal(res.data.length) } catch (error) { toast({ title: "Error loading MCP servers", description: "Unable to load servers.", variant: "destructive", }) } finally { setIsLoading(false) } } fetchServers() }, [page, limit]) // Search server by name/description (local filter) const filteredServers = Array.isArray(mcpServers) ? mcpServers.filter( (server) => server.name.toLowerCase().includes(searchQuery.toLowerCase()) || (server.description || "").toLowerCase().includes(searchQuery.toLowerCase()), ) : [] const handleAddHeader = () => { setServerData({ ...serverData, headers: [...serverData.headers, { key: "", value: "" }], }) } const handleRemoveHeader = (index: number) => { const updatedHeaders = [...serverData.headers] updatedHeaders.splice(index, 1) setServerData({ ...serverData, headers: updatedHeaders, }) } const handleHeaderChange = (index: number, field: "key" | "value", value: string) => { const updatedHeaders = [...serverData.headers] updatedHeaders[index][field] = value setServerData({ ...serverData, headers: updatedHeaders, }) } const handleAddEnvironment = () => { setServerData({ ...serverData, environments: [...serverData.environments, { key: "" }], }) } const handleRemoveEnvironment = (index: number) => { const updatedEnvironments = [...serverData.environments] updatedEnvironments.splice(index, 1) setServerData({ ...serverData, environments: updatedEnvironments, }) } const handleEnvironmentChange = (index: number, value: string) => { const updatedEnvironments = [...serverData.environments] updatedEnvironments[index].key = value setServerData({ ...serverData, environments: updatedEnvironments, }) } const handleAddTool = () => { const name = "new_tool"; const newTool: ToolConfig = { id: name, name: name, description: "", tags: [], examples: [], inputModes: ["text"], outputModes: ["text"], } setServerData({ ...serverData, tools: [...serverData.tools, newTool], }) } const handleRemoveTool = (index: number) => { const updatedTools = [...serverData.tools] updatedTools.splice(index, 1) setServerData({ ...serverData, tools: updatedTools, }) } const handleToolChange = (index: number, field: keyof ToolConfig, value: any) => { const updatedTools = [...serverData.tools] updatedTools[index] = { ...updatedTools[index], [field]: value, } if (field === 'name') { updatedTools[index].id = value; } setServerData({ ...serverData, tools: updatedTools, }) } const handleAddServer = async (e: React.FormEvent) => { e.preventDefault() setIsLoading(true) try { // Convert environments array to object const environmentsObj: Record = {} serverData.environments.forEach((env) => { if (env.key) { environmentsObj[env.key] = `env@@${env.key}` } }) // Convert headers array to object const headersObj: Record = {} serverData.headers.forEach((header) => { if (header.key) { headersObj[header.key] = header.value } }) let config_json: any = {} if (serverData.config_type === "sse") { config_json = { url: serverData.url, headers: headersObj, } } else if (serverData.config_type === "studio") { const args = serverData.args.split("\n").filter((arg) => arg.trim() !== "") const envObj: Record = {} serverData.environments.forEach((env) => { if (env.key) { envObj[env.key] = `env@@${env.key}` } }) config_json = { command: serverData.command, args: args, env: envObj, } } const payload: MCPServerCreate = { name: serverData.name, description: serverData.description, type: serverData.type, config_type: serverData.config_type, config_json, environments: environmentsObj, tools: serverData.tools, } if (selectedServer) { await updateMCPServer(selectedServer.id, payload) toast({ title: "Server updated", description: `${serverData.name} was updated successfully.`, }) } else { await createMCPServer(payload) toast({ title: "Server added", description: `${serverData.name} was added successfully.`, }) } setIsDialogOpen(false) resetForm() // Reload list const res = await listMCPServers((page - 1) * limit, limit) setMcpServers(res.data) setTotal(res.data.length) } catch (error) { toast({ title: "Error", description: "Unable to save the MCP server. Please try again.", variant: "destructive", }) } finally { setIsLoading(false) } } const handleEditServer = async (server: MCPServer) => { setIsLoading(true) try { const res = await getMCPServer(server.id) setSelectedServer(res.data) // Convert environments object to array const environmentsArray = Object.keys(res.data.environments || {}).map((key) => ({ key })) // Convert headers object to array const headersArray = res.data.config_json.headers ? Object.entries(res.data.config_json.headers).map(([key, value]) => ({ key, value: value as string })) : [{ key: "x-api-key", value: "" }] if (res.data.config_type === "sse") { setServerData({ name: res.data.name, description: res.data.description || "", type: res.data.type, config_type: res.data.config_type as any, url: res.data.config_json.url || "", headers: headersArray, command: "", args: "", environments: environmentsArray, tools: res.data.tools, }) } else if (res.data.config_type === "studio") { setServerData({ name: res.data.name, description: res.data.description || "", type: res.data.type, config_type: res.data.config_type as any, url: "", headers: [], command: res.data.config_json.command || "npx", args: (res.data.config_json.args || []).join("\n"), environments: environmentsArray, tools: res.data.tools, }) } setActiveTab("basic") setIsDialogOpen(true) } catch (error) { toast({ title: "Error searching MCP server", description: "Unable to search the server.", variant: "destructive", }) } finally { setIsLoading(false) } } const handleDeleteServer = (server: MCPServer) => { setSelectedServer(server) setIsDeleteDialogOpen(true) } const confirmDeleteServer = async () => { if (!selectedServer) return setIsLoading(true) try { await deleteMCPServer(selectedServer.id) toast({ title: "Server deleted", description: `${selectedServer.name} was deleted successfully.`, }) setIsDeleteDialogOpen(false) setSelectedServer(null) // Reload list const res = await listMCPServers((page - 1) * limit, limit) setMcpServers(res.data) setTotal(res.data.length) } catch (error) { toast({ title: "Error", description: "Unable to delete the server. Please try again.", variant: "destructive", }) } finally { setIsLoading(false) } } const resetForm = () => { setServerData({ name: "", description: "", type: "official", config_type: "sse", url: "", headers: [{ key: "x-api-key", value: "" }], command: "npx", args: "", environments: [], tools: [], }) setSelectedServer(null) setActiveTab("basic") } return (

MCP Servers Management

{selectedServer ? "Edit MCP Server" : "New MCP Server"} {selectedServer ? "Edit the existing MCP server information." : "Fill in the information to create a new MCP server."} Basic Information Environment Variables Tools
setServerData({ ...serverData, name: e.target.value })} className="bg-[#222] border-[#444] text-white" placeholder="MCP Server Name" required />