feat: add server editing functionality and enhance server management features

This commit is contained in:
samanhappy
2025-04-05 16:28:58 +08:00
parent ffd6b93c2c
commit 3669260410
7 changed files with 557 additions and 258 deletions

View File

@@ -12,6 +12,13 @@
},
"rules": {
"no-console": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"no-undef": "off"
}
}

View File

@@ -58,8 +58,39 @@ function ToolCard({ tool }) {
);
}
// Delete confirmation dialog component
function DeleteDialog({ isOpen, onClose, onConfirm, serverName }) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div className="bg-white shadow rounded-lg p-6 w-full max-w-md">
<h3 className="text-lg font-medium text-gray-900 mb-4">Confirm Deletion</h3>
<p className="text-gray-700">
Are you sure you want to delete the server <strong>{serverName}</strong>? This action
cannot be undone.
</p>
<div className="flex justify-end mt-6">
<button
onClick={onClose}
className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-medium py-2 px-4 rounded mr-2"
>
Cancel
</button>
<button
onClick={onConfirm}
className="bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded"
>
Delete
</button>
</div>
</div>
</div>
);
}
// 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 }) {
<Badge status={server.status} />
</div>
<div className="flex space-x-2">
<button
onClick={handleEdit}
className="px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm"
>
Edit
</button>
<button
onClick={handleRemove}
className="px-3 py-1 bg-red-100 text-red-800 rounded hover:bg-red-200 text-sm"
@@ -127,19 +169,33 @@ function ServerCard({ server, onRemove }) {
);
}
// Form component for adding new MCP servers with stdio or SSE protocol support
function AddServerForm({ onAdd }) {
const [modalVisible, setModalVisible] = useState(false);
const [serverType, setServerType] = useState('stdio');
// Form component for adding/editing MCP servers with stdio or SSE protocol support
function ServerForm({ onSubmit, onCancel, initialData = null, modalTitle }) {
const [serverType, setServerType] = useState(
initialData && initialData.config && initialData.config.url ? 'sse' : 'stdio',
);
const [formData, setFormData] = useState({
name: '',
url: '',
command: '',
arguments: '',
args: [],
name: (initialData && initialData.name) || '',
url: (initialData && initialData.config && initialData.config.url) || '',
command: (initialData && initialData.config && initialData.config.command) || '',
arguments:
initialData && initialData.config && initialData.config.args
? Array.isArray(initialData.config.args)
? initialData.config.args.join(' ')
: String(initialData.config.args)
: '',
args: (initialData && initialData.config && initialData.config.args) || [],
});
const [envVars, setEnvVars] = useState([]);
const [envVars, setEnvVars] = useState(
initialData && initialData.config && initialData.config.env
? Object.entries(initialData.config.env).map(([key, value]) => ({ key, value }))
: [],
);
const [error, setError] = useState(null);
const isEdit = !!initialData;
const handleInputChange = (e) => {
const { name, value } = e.target;
@@ -168,14 +224,6 @@ function AddServerForm({ onAdd }) {
setEnvVars(newEnvVars);
};
const toggleModal = () => {
setModalVisible(!modalVisible);
setError(null);
if (!modalVisible) {
setEnvVars([]);
}
};
// Submit handler for server configuration
const handleSubmit = async (e) => {
e.preventDefault();
@@ -194,13 +242,202 @@ function AddServerForm({ onAdd }) {
config:
serverType === 'sse'
? { url: formData.url }
: {
command: formData.command,
: {
command: formData.command,
args: formData.args,
env: Object.keys(env).length > 0 ? env : undefined
env: Object.keys(env).length > 0 ? env : undefined,
},
};
onSubmit(payload);
} catch (err) {
setError('Error: ' + err.message);
}
};
return (
<div className="bg-white shadow rounded-lg p-6 w-full max-w-xl max-h-screen overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900">{modalTitle}</h2>
<button onClick={onCancel} className="text-gray-500 hover:text-gray-700">
</button>
</div>
{error && <div className="bg-red-50 text-red-700 p-3 rounded mb-4">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
Server Name
</label>
<input
type="text"
name="name"
id="name"
value={formData.name}
onChange={handleInputChange}
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., time-mcp"
required
disabled={isEdit}
/>
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">Server Type</label>
<div className="flex space-x-4">
<div>
<input
type="radio"
id="command"
name="serverType"
value="command"
checked={serverType === 'stdio'}
onChange={() => setServerType('stdio')}
className="mr-1"
/>
<label htmlFor="command">stdio</label>
</div>
<div>
<input
type="radio"
id="url"
name="serverType"
value="url"
checked={serverType === 'sse'}
onChange={() => setServerType('sse')}
className="mr-1"
/>
<label htmlFor="url">sse</label>
</div>
</div>
</div>
{serverType === 'sse' ? (
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url">
Server URL
</label>
<input
type="url"
name="url"
id="url"
value={formData.url}
onChange={handleInputChange}
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., http://localhost:3000/sse"
required={serverType === 'sse'}
/>
</div>
) : (
<Fragment>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="command">
Command
</label>
<input
type="text"
name="command"
id="command"
value={formData.command}
onChange={handleInputChange}
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., npx"
required={serverType === 'stdio'}
/>
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="arguments">
Arguments
</label>
<input
type="text"
name="arguments"
id="arguments"
value={formData.arguments}
onChange={(e) => 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'}
/>
</div>
<div className="mb-4">
<div className="flex justify-between items-center mb-2">
<label className="block text-gray-700 text-sm font-bold">
Environment Variables
</label>
<button
type="button"
onClick={addEnvVar}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center"
>
+ Add
</button>
</div>
{envVars.map((envVar, index) => (
<div key={index} className="flex items-center mb-2">
<div className="flex items-center space-x-2 flex-grow">
<input
type="text"
value={envVar.key}
onChange={(e) => 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"
/>
<span className="flex items-center">:</span>
<input
type="text"
value={envVar.value}
onChange={(e) => 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"
/>
</div>
<button
type="button"
onClick={() => removeEnvVar(index)}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[56px] ml-2"
>
- Del
</button>
</div>
))}
</div>
</Fragment>
)}
<div className="flex justify-end mt-6">
<button
type="button"
onClick={onCancel}
className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-medium py-2 px-4 rounded mr-2"
>
Cancel
</button>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
>
{isEdit ? 'Save Changes' : 'Add Server'}
</button>
</div>
</form>
</div>
);
}
// 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 && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div className="bg-white shadow rounded-lg p-6 w-full max-w-xl max-h-screen overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900">Add New Server</h2>
<button onClick={toggleModal} className="text-gray-500 hover:text-gray-700">
</button>
</div>
{error && <div className="bg-red-50 text-red-700 p-3 rounded mb-4">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
Server Name
</label>
<input
type="text"
name="name"
id="name"
value={formData.name}
onChange={handleInputChange}
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., time-mcp"
required
/>
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">Server Type</label>
<div className="flex space-x-4">
<div>
<input
type="radio"
id="command"
name="serverType"
value="command"
checked={serverType === 'stdio'}
onChange={() => setServerType('stdio')}
className="mr-1"
/>
<label htmlFor="command">stdio</label>
</div>
<div>
<input
type="radio"
id="url"
name="serverType"
value="url"
checked={serverType === 'sse'}
onChange={() => setServerType('sse')}
className="mr-1"
/>
<label htmlFor="url">sse</label>
</div>
</div>
</div>
{serverType === 'sse' ? (
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url">
Server URL
</label>
<input
type="url"
name="url"
id="url"
value={formData.url}
onChange={handleInputChange}
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., http://localhost:3000/sse"
required={serverType === 'sse'}
/>
</div>
) : (
<Fragment>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="command">
Command
</label>
<input
type="text"
name="command"
id="command"
value={formData.command}
onChange={handleInputChange}
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., npx"
required={serverType === 'stdio'}
/>
</div>
<div className="mb-4">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="arguments"
>
Arguments
</label>
<input
type="text"
name="arguments"
id="arguments"
value={formData.arguments}
onChange={(e) => 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'}
/>
</div>
<div className="mb-4">
<div className="flex justify-between items-center mb-2">
<label className="block text-gray-700 text-sm font-bold">
Environment Variables
</label>
<button
type="button"
onClick={addEnvVar}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center"
>
+ Add
</button>
</div>
{envVars.map((envVar, index) => (
<div key={index} className="flex items-center mb-2">
<div className="flex items-center space-x-2 flex-grow">
<input
type="text"
value={envVar.key}
onChange={(e) => 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"
/>
<span className="flex items-center">:</span>
<input
type="text"
value={envVar.value}
onChange={(e) => 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"
/>
</div>
<button
type="button"
onClick={() => removeEnvVar(index)}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[56px] ml-2"
>
- Del
</button>
</div>
))}
</div>
</Fragment>
)}
<div className="flex justify-end mt-6">
<button
type="button"
onClick={toggleModal}
className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-medium py-2 px-4 rounded mr-2"
>
Cancel
</button>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
>
Add Server
</button>
</div>
</form>
</div>
<ServerForm onSubmit={handleSubmit} onCancel={toggleModal} modalTitle="Add New Server" />
</div>
)}
</div>
);
}
// 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 (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<ServerForm
onSubmit={handleSubmit}
onCancel={onCancel}
initialData={server}
modalTitle={`Edit Server: ${server.name}`}
/>
</div>
);
}
// 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() {
<h1 className="text-3xl font-bold text-gray-900">MCP Hub Dashboard</h1>
<AddServerForm onAdd={handleServerAdd} />
</div>
{servers.length === 0 ? (
<div className="bg-white shadow rounded-lg p-6">
<p className="text-gray-600">No MCP servers available</p>
@@ -522,10 +652,22 @@ function App() {
) : (
<div className="space-y-6">
{servers.map((server, index) => (
<ServerCard key={index} server={server} onRemove={handleServerRemove} />
<ServerCard
key={index}
server={server}
onRemove={handleServerRemove}
onEdit={handleServerEdit}
/>
))}
</div>
)}
{editingServer && (
<EditServerForm
server={editingServer}
onEdit={handleEditComplete}
onCancel={() => setEditingServer(null)}
/>
)}
</div>
</div>
);

View File

@@ -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<McpServer> => {
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<void> =
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<void> =
});
}
};
export const updateServer = async (req: Request, res: Response): Promise<void> => {
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',
});
}
};

View File

@@ -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);

View File

@@ -19,7 +19,7 @@ export class AppServer {
async initialize(): Promise<void> {
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));

View File

@@ -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<string, string> = {};
for (const key in rawEnv) {
if (typeof rawEnv[key] === 'string') {
env[key] = expandEnvVars(rawEnv[key] as string);
}
}
const env: Record<string, string> = 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<void> => {
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<void> => {
tool.description || '',
cast(tool.inputSchema.properties),
async (params: Record<string, unknown>) => {
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<void> => {
// Get all server information
export const getServersInfo = (): Omit<ServerInfo, 'client' | 'transport'>[] => {
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<void> => {
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 });

View File

@@ -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<T = unknown> {
export interface AddServerRequest {
name: string; // Name of the server to add
config: ServerConfig; // Configuration details for the server
}
}