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 }) {
+
);
}
+// 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
+}