diff --git a/.eslintrc.json b/.eslintrc.json index d284031..66548e7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,6 +12,13 @@ }, "rules": { "no-console": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ], "no-undef": "off" } } diff --git a/public/js/app.js b/public/js/app.js index 763192b..400e9ee 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -58,8 +58,39 @@ function ToolCard({ tool }) { ); } +// Delete confirmation dialog component +function DeleteDialog({ isOpen, onClose, onConfirm, serverName }) { + if (!isOpen) return null; + + return ( +
+
+

Confirm Deletion

+

+ Are you sure you want to delete the server {serverName}? This action + cannot be undone. +

+
+ + +
+
+
+ ); +} + // Main server card component for displaying server status and available tools -function ServerCard({ server, onRemove }) { +function ServerCard({ server, onRemove, onEdit }) { const [isExpanded, setIsExpanded] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); @@ -68,6 +99,11 @@ function ServerCard({ server, onRemove }) { setShowDeleteDialog(true); }; + const handleEdit = (e) => { + e.stopPropagation(); + onEdit(server); + }; + const handleConfirmDelete = () => { onRemove(server.name); setShowDeleteDialog(false); @@ -84,6 +120,12 @@ function ServerCard({ server, onRemove }) {
+ +
+ + {error &&
{error}
} + +
+
+ + +
+ +
+ +
+
+ setServerType('stdio')} + className="mr-1" + /> + +
+
+ setServerType('sse')} + className="mr-1" + /> + +
+
+
+ + {serverType === 'sse' ? ( +
+ + +
+ ) : ( + +
+ + +
+
+ + handleArgsChange(e.target.value)} + className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" + placeholder="e.g., -y time-mcp" + required={serverType === 'stdio'} + /> +
+ +
+
+ + +
+ {envVars.map((envVar, index) => ( +
+
+ handleEnvVarChange(index, 'key', e.target.value)} + className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2" + placeholder="key" + /> + : + handleEnvVarChange(index, 'value', e.target.value)} + className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2" + placeholder="value" + /> +
+ +
+ ))} +
+
+ )} + +
+ + +
+
+ + ); +} + +// Form component for adding new MCP servers (wrapper around ServerForm) +function AddServerForm({ onAdd }) { + const [modalVisible, setModalVisible] = useState(false); + + const toggleModal = () => { + setModalVisible(!modalVisible); + }; + + const handleSubmit = async (payload) => { + try { const response = await fetch('/api/servers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -210,23 +447,14 @@ function AddServerForm({ onAdd }) { const result = await response.json(); if (!response.ok) { - setError(result.message || 'Failed to add server'); + alert(result.message || 'Failed to add server'); return; } - setFormData({ - name: '', - url: '', - command: '', - arguments: '', - args: [], - }); - setEnvVars([]); setModalVisible(false); - onAdd(); } catch (err) { - setError('Error: ' + err.message); + alert('Error: ' + err.message); } }; @@ -241,188 +469,54 @@ function AddServerForm({ onAdd }) { {modalVisible && (
-
-
-

Add New Server

- -
- - {error &&
{error}
} - -
-
- - -
- -
- -
-
- setServerType('stdio')} - className="mr-1" - /> - -
-
- setServerType('sse')} - className="mr-1" - /> - -
-
-
- - {serverType === 'sse' ? ( -
- - -
- ) : ( - -
- - -
-
- - handleArgsChange(e.target.value)} - className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" - placeholder="e.g., -y time-mcp" - required={serverType === 'stdio'} - /> -
- -
-
- - -
- {envVars.map((envVar, index) => ( -
-
- handleEnvVarChange(index, 'key', e.target.value)} - className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2" - placeholder="key" - /> - : - handleEnvVarChange(index, 'value', e.target.value)} - className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2" - placeholder="value" - /> -
- -
- ))} -
-
- )} - -
- - -
-
-
+
)} ); } +// Form component for editing MCP servers (wrapper around ServerForm) +function EditServerForm({ server, onEdit, onCancel }) { + const handleSubmit = async (payload) => { + try { + const response = await fetch(`/api/servers/${server.name}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + const result = await response.json(); + + if (!response.ok) { + alert(result.message || 'Failed to update server'); + return; + } + + onEdit(); + } catch (err) { + alert('Error: ' + err.message); + } + }; + + return ( +
+ +
+ ); +} + // Root application component managing server state and UI function App() { const [servers, setServers] = useState([]); const [error, setError] = useState(null); const [refreshKey, setRefreshKey] = useState(0); + const [editingServer, setEditingServer] = useState(null); useEffect(() => { fetch('/api/servers') @@ -469,12 +563,49 @@ function App() { setRefreshKey((prevKey) => prevKey + 1); }; + const handleServerEdit = (server) => { + // Fetch settings to get the full server config before editing + fetch(`/api/settings`) + .then((response) => response.json()) + .then((settingsData) => { + if ( + settingsData && + settingsData.success && + settingsData.data && + settingsData.data.mcpServers && + settingsData.data.mcpServers[server.name] + ) { + const serverConfig = settingsData.data.mcpServers[server.name]; + const fullServerData = { + name: server.name, + status: server.status, + tools: server.tools || [], + config: serverConfig, + }; + + console.log('Editing server with config:', fullServerData); + setEditingServer(fullServerData); + } else { + console.error('Failed to get server config from settings:', settingsData); + setError(`Could not find configuration data for ${server.name}`); + } + }) + .catch((err) => { + console.error('Error fetching server settings:', err); + setError(err.message); + }); + }; + + const handleEditComplete = () => { + setEditingServer(null); + setRefreshKey((prevKey) => prevKey + 1); + }; + const handleServerRemove = async (serverName) => { try { const response = await fetch(`/api/servers/${serverName}`, { method: 'DELETE', }); - const result = await response.json(); if (!response.ok) { @@ -514,7 +645,6 @@ function App() {

MCP Hub Dashboard

- {servers.length === 0 ? (

No MCP servers available

@@ -522,10 +652,22 @@ function App() { ) : (
{servers.map((server, index) => ( - + ))}
)} + {editingServer && ( + setEditingServer(null)} + /> + )}
); diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index e166b86..9e4f901 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -1,7 +1,14 @@ import { Request, Response } from 'express'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { ApiResponse, AddServerRequest } from '../types/index.js'; -import { getServersInfo, addServer, removeServer, createMcpServer, registerAllTools } from '../services/mcpService.js'; +import { + getServersInfo, + addServer, + removeServer, + createMcpServer, + registerAllTools, + updateMcpServer, +} from '../services/mcpService.js'; import { loadSettings } from '../config/index.js'; import config from '../config/index.js'; @@ -14,25 +21,16 @@ export const setMcpServerInstance = (server: McpServer): void => { // 重新创建 McpServer 实例 export const recreateMcpServerInstance = async (): Promise => { console.log('Re-creating McpServer instance'); - - // 如果存在旧的实例,尝试关闭它(如果有相关的关闭方法) - if (mcpServerInstance && typeof mcpServerInstance.close === 'function') { - try { - await mcpServerInstance.close(); - } catch (error) { - console.error('Error closing existing McpServer instance:', error); - } - } - + // 创建新的 McpServer 实例 const newServer = createMcpServer(config.mcpHubName, config.mcpHubVersion); - - // 更新全局实例 - mcpServerInstance = newServer; - + // 重新注册所有工具 - await registerAllTools(mcpServerInstance); - + await registerAllTools(newServer); + + // 更新全局实例 + mcpServerInstance.close(); + mcpServerInstance = newServer; console.log('McpServer instance successfully re-created'); return mcpServerInstance; }; @@ -135,19 +133,11 @@ export const deleteServer = async (req: Request, res: Response): Promise = if (result.success) { // 重新创建 McpServer 实例 - try { - await recreateMcpServerInstance(); - res.json({ - success: true, - message: 'Server removed successfully and McpServer re-created' - }); - } catch (error) { - console.error('Failed to re-create McpServer after removing server:', error); - res.json({ - success: true, - message: 'Server removed successfully but failed to re-create McpServer' - }); - } + recreateMcpServerInstance(); + res.json({ + success: true, + message: 'Server removed successfully', + }); } else { res.status(404).json({ success: false, @@ -161,3 +151,89 @@ export const deleteServer = async (req: Request, res: Response): Promise = }); } }; + +export const updateServer = async (req: Request, res: Response): Promise => { + try { + const { name } = req.params; + const { config } = req.body; + + if (!name) { + res.status(400).json({ + success: false, + message: 'Server name is required', + }); + return; + } + + if (!config || typeof config !== 'object') { + res.status(400).json({ + success: false, + message: 'Server configuration is required', + }); + return; + } + + if (!config.url && (!config.command || !config.args)) { + res.status(400).json({ + success: false, + message: 'Server configuration must include either a URL or command with arguments', + }); + return; + } + + const result = await updateMcpServer(mcpServerInstance, name, config); + + if (result.success) { + recreateMcpServerInstance(); + res.json({ + success: true, + message: 'Server updated successfully', + }); + } else { + res.status(404).json({ + success: false, + message: result.message || 'Server not found or failed to update', + }); + } + } catch (error) { + res.status(500).json({ + success: false, + message: 'Internal server error', + }); + } +}; + +export const getServerConfig = (req: Request, res: Response): void => { + try { + const { name } = req.params; + const settings = loadSettings(); + + if (!settings.mcpServers || !settings.mcpServers[name]) { + res.status(404).json({ + success: false, + message: 'Server not found', + }); + return; + } + + const serverInfo = getServersInfo().find((s) => s.name === name); + const serverConfig = settings.mcpServers[name]; + + const response: ApiResponse = { + success: true, + data: { + name, + status: serverInfo ? serverInfo.status : 'disconnected', + tools: serverInfo ? serverInfo.tools : [], + config: serverConfig, + }, + }; + + res.json(response); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Failed to get server configuration', + }); + } +}; diff --git a/src/routes/index.ts b/src/routes/index.ts index 9348676..3c26dd4 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -3,6 +3,7 @@ import { getAllServers, getAllSettings, createServer, + updateServer, deleteServer, setMcpServerInstance } from '../controllers/serverController.js'; @@ -16,6 +17,7 @@ export const initRoutes = (app: express.Application, server: McpServer): void => router.get('/servers', getAllServers); router.get('/settings', getAllSettings); router.post('/servers', createServer); + router.put('/servers/:name', updateServer); router.delete('/servers/:name', deleteServer); app.use('/api', router); diff --git a/src/server.ts b/src/server.ts index 9cc973e..d16e4af 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,7 +19,7 @@ export class AppServer { async initialize(): Promise { try { - registerAllTools(this.mcpServer); + await registerAllTools(this.mcpServer); initMiddlewares(this.app); initRoutes(this.app, this.mcpServer); this.app.get('/sse', (req, res) => handleSseConnection(req, res, this.mcpServer)); diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index 0580d93..ec327d0 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -7,6 +7,7 @@ import * as z from 'zod'; import { ZodType, ZodRawShape } from 'zod'; import { ServerInfo, ServerConfig } from '../types/index.js'; import { loadSettings, saveSettings, expandEnvVars } from '../config/index.js'; +import { exec } from 'child_process'; // Store all server information let serverInfos: ServerInfo[] = []; @@ -32,15 +33,8 @@ export const initializeClientsFromSettings = (): ServerInfo[] => { if (config.url) { transport = new SSEClientTransport(new URL(config.url)); } else if (config.command && config.args) { - const rawEnv = { ...process.env, ...(config.env || {}) }; - const env: Record = {}; - - for (const key in rawEnv) { - if (typeof rawEnv[key] === 'string') { - env[key] = expandEnvVars(rawEnv[key] as string); - } - } - + const env: Record = config.env || {}; + env['PATH'] = expandEnvVars(process.env.PATH as string) || ''; transport = new StdioClientTransport({ command: config.command, args: config.args, @@ -52,6 +46,7 @@ export const initializeClientsFromSettings = (): ServerInfo[] => { name, status: 'disconnected', tools: [], + createTime: Date.now(), }); continue; } @@ -69,13 +64,20 @@ export const initializeClientsFromSettings = (): ServerInfo[] => { }, }, ); - + client.connect(transport).catch((error) => { + console.error(`Failed to connect client for server ${name} by error: ${error}`); + const serverInfo = getServerInfoByName(name); + if (serverInfo) { + serverInfo.status = 'disconnected'; + } + }); serverInfos.push({ name, status: 'connecting', tools: [], client, transport, + createTime: Date.now(), }); console.log(`Initialized client for server: ${name}`); } @@ -94,9 +96,7 @@ export const registerAllTools = async (server: McpServer): Promise => { serverInfo.status = 'connecting'; console.log(`Connecting to server: ${serverInfo.name}...`); - await serverInfo.client.connect(serverInfo.transport); const tools = await serverInfo.client.listTools(); - serverInfo.tools = tools.tools.map((tool) => ({ name: tool.name, description: tool.description || '', @@ -114,14 +114,13 @@ export const registerAllTools = async (server: McpServer): Promise => { tool.description || '', cast(tool.inputSchema.properties), async (params: Record) => { + const currentServer = getServerInfoByName(serverInfo.name)!; console.log(`Calling tool: ${tool.name} with params: ${JSON.stringify(params)}`); - - const result = await serverInfo.client!.callTool({ + const result = await currentServer.client!.callTool({ name: tool.name, arguments: params, }); - - console.log(`Tool result: ${JSON.stringify(result)}`); + console.log(`Tool call result: ${JSON.stringify(result)}`); return result as CallToolResult; }, ); @@ -137,13 +136,19 @@ export const registerAllTools = async (server: McpServer): Promise => { // Get all server information export const getServersInfo = (): Omit[] => { - return serverInfos.map(({ name, status, tools }) => ({ + return serverInfos.map(({ name, status, tools, createTime }) => ({ name, status, tools, + createTime, })); }; +// Get server information by name +const getServerInfoByName = (name: string): ServerInfo | undefined => { + return serverInfos.find((serverInfo) => serverInfo.name === name); +}; + // Add new server export const addServer = async ( mcpServer: McpServer, @@ -175,7 +180,7 @@ export const addServer = async ( // Remove server export const removeServer = ( name: string, - mcpServer?: McpServer + mcpServer?: McpServer, ): { success: boolean; message?: string } => { try { const settings = loadSettings(); @@ -203,7 +208,7 @@ export const removeServer = ( // Re-create and initialize the McpServer if provided if (mcpServer) { console.log(`Re-initializing McpServer after removing ${name}`); - registerAllTools(mcpServer).catch(error => { + registerAllTools(mcpServer).catch((error) => { console.error(`Error re-initializing McpServer after removing ${name}:`, error); }); } @@ -215,6 +220,72 @@ export const removeServer = ( } }; +// Update existing server +export const updateMcpServer = async ( + mcpServer: McpServer, + name: string, + config: ServerConfig, +): Promise<{ success: boolean; message?: string }> => { + try { + const settings = loadSettings(); + + if (!settings.mcpServers[name]) { + return { success: false, message: 'Server not found' }; + } + + // Update server configuration + settings.mcpServers[name] = config; + + if (!saveSettings(settings)) { + return { success: false, message: 'Failed to save settings' }; + } + + // Close existing connections if any + const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name); + if (serverInfo && serverInfo.client) { + serverInfo.transport?.close(); + // serverInfo.transport = undefined; + serverInfo.client.close(); + // serverInfo.client = undefined; + console.log(`Closed existing connection for server: ${name}`); + + // kill process + // await killProcess(serverInfo); + } + + // Remove from list + serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name); + console.log(`Server Infos after removing: ${JSON.stringify(serverInfos)}`); + + return { success: true, message: 'Server updated successfully' }; + } catch (error) { + console.error(`Failed to update server: ${name}`, error); + return { success: false, message: 'Failed to update server' }; + } +}; + +// Kill process by name +export const killProcess = (serverInfo: ServerInfo): Promise => { + return new Promise((resolve, _) => { + exec(`pkill -9 "${serverInfo.name}"`, (error, stdout, stderr) => { + if (error) { + console.error(`Error killing process ${serverInfo.name}:`, error); + // Don't reject on error since pkill returns error if no process is found + resolve(); + return; + } + if (stderr) { + console.error(`Error killing process ${serverInfo.name}:`, stderr); + // Don't reject on stderr output as it might just be warnings + resolve(); + return; + } + console.log(`Process ${serverInfo.name} killed successfully`); + resolve(); + }); + }); +}; + // Create McpServer instance export const createMcpServer = (name: string, version: string): McpServer => { return new McpServer({ name, version }); diff --git a/src/types/index.ts b/src/types/index.ts index 1f138ff..bada9b8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -24,6 +24,7 @@ export interface ServerInfo { tools: ToolInfo[]; // List of tools available on the server client?: Client; // Client instance for communication transport?: SSEClientTransport | StdioClientTransport; // Transport mechanism used + createTime: number; // Timestamp of when the server was created } // Details about a tool available on the server @@ -44,4 +45,4 @@ export interface ApiResponse { export interface AddServerRequest { name: string; // Name of the server to add config: ServerConfig; // Configuration details for the server -} \ No newline at end of file +}