mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
refactor: streamline server initialization and enhance error handling in serverController
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { ApiResponse, AddServerRequest } from '../types/index.js';
|
||||
import { getServersInfo, addServer, removeServer } from '../services/mcpService.js';
|
||||
import { loadSettings } from '../config/index.js';
|
||||
|
||||
let mcpServerInstance: any;
|
||||
let mcpServerInstance: McpServer;
|
||||
|
||||
export const setMcpServerInstance = (server: any): void => {
|
||||
export const setMcpServerInstance = (server: McpServer): void => {
|
||||
mcpServerInstance = server;
|
||||
};
|
||||
|
||||
@@ -14,13 +15,13 @@ export const getAllServers = (_: Request, res: Response): void => {
|
||||
const serversInfo = getServersInfo();
|
||||
const response: ApiResponse = {
|
||||
success: true,
|
||||
data: serversInfo
|
||||
data: serversInfo,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to get servers information'
|
||||
message: 'Failed to get servers information',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -30,13 +31,13 @@ export const getAllSettings = (_: Request, res: Response): void => {
|
||||
const settings = loadSettings();
|
||||
const response: ApiResponse = {
|
||||
success: true,
|
||||
data: settings
|
||||
data: settings,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to get server settings'
|
||||
message: 'Failed to get server settings',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -46,17 +47,17 @@ export const createServer = async (req: Request, res: Response): Promise<void> =
|
||||
const { name, config } = req.body as AddServerRequest;
|
||||
|
||||
if (!name || typeof name !== 'string') {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server name is required'
|
||||
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'
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server configuration is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -64,28 +65,28 @@ export const createServer = async (req: Request, res: Response): Promise<void> =
|
||||
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'
|
||||
message: 'Server configuration must include either a URL or command with arguments',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await addServer(mcpServerInstance, name, config);
|
||||
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Server added successfully'
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Server added successfully',
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: result.message || 'Failed to add server'
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: result.message || 'Failed to add server',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -93,32 +94,32 @@ export const createServer = async (req: Request, res: Response): Promise<void> =
|
||||
export const deleteServer = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { name } = req.params;
|
||||
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server name is required'
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server name is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = removeServer(name);
|
||||
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Server removed successfully'
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Server removed successfully',
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: result.message || 'Server not found or failed to remove'
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: result.message || 'Server not found or failed to remove',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,15 +8,15 @@ export const errorHandler = (
|
||||
_next: NextFunction
|
||||
): void => {
|
||||
console.error('Unhandled error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error',
|
||||
});
|
||||
};
|
||||
|
||||
export const initMiddlewares = (app: express.Application): void => {
|
||||
app.use(express.static('public'));
|
||||
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.path !== '/sse' && req.path !== '/messages') {
|
||||
express.json()(req, res, next);
|
||||
@@ -30,4 +30,4 @@ export const initMiddlewares = (app: express.Application): void => {
|
||||
});
|
||||
|
||||
app.use(errorHandler);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ export class AppServer {
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
await registerAllTools(this.mcpServer);
|
||||
registerAllTools(this.mcpServer);
|
||||
initMiddlewares(this.app);
|
||||
initRoutes(this.app, this.mcpServer);
|
||||
this.app.get('/sse', (req, res) => handleSseConnection(req, res, this.mcpServer));
|
||||
|
||||
@@ -5,7 +5,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||
import * as z from 'zod';
|
||||
import { ZodType, ZodRawShape } from 'zod';
|
||||
import { ServerInfo, ServerConfig, ToolInfo } from '../types/index.js';
|
||||
import { ServerInfo, ServerConfig } from '../types/index.js';
|
||||
import { loadSettings, saveSettings, expandEnvVars } from '../config/index.js';
|
||||
|
||||
// Store all server information
|
||||
@@ -19,7 +19,9 @@ export const initializeClientsFromSettings = (): ServerInfo[] => {
|
||||
|
||||
for (const [name, config] of Object.entries(settings.mcpServers)) {
|
||||
// Check if server is already connected
|
||||
const existingServer = existingServerInfos.find(s => s.name === name && s.status === 'connected');
|
||||
const existingServer = existingServerInfos.find(
|
||||
(s) => s.name === name && s.status === 'connected',
|
||||
);
|
||||
if (existingServer) {
|
||||
serverInfos.push(existingServer);
|
||||
console.log(`Server '${name}' is already connected.`);
|
||||
@@ -32,13 +34,13 @@ export const initializeClientsFromSettings = (): ServerInfo[] => {
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
transport = new StdioClientTransport({
|
||||
command: config.command,
|
||||
args: config.args,
|
||||
@@ -83,6 +85,7 @@ export const initializeClientsFromSettings = (): ServerInfo[] => {
|
||||
|
||||
// Register all MCP tools
|
||||
export const registerAllTools = async (server: McpServer): Promise<void> => {
|
||||
initializeClientsFromSettings();
|
||||
for (const serverInfo of serverInfos) {
|
||||
if (serverInfo.status === 'connected') continue;
|
||||
if (!serverInfo.client || !serverInfo.transport) continue;
|
||||
@@ -90,41 +93,43 @@ export const registerAllTools = async (server: McpServer): Promise<void> => {
|
||||
try {
|
||||
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 || '',
|
||||
inputSchema: tool.inputSchema.properties || {},
|
||||
}));
|
||||
|
||||
|
||||
serverInfo.status = 'connected';
|
||||
console.log(`Successfully connected to server: ${serverInfo.name}`);
|
||||
|
||||
|
||||
for (const tool of tools.tools) {
|
||||
console.log(`Registering tool: ${JSON.stringify(tool)}`);
|
||||
|
||||
|
||||
await server.tool(
|
||||
tool.name,
|
||||
tool.description || '',
|
||||
cast(tool.inputSchema.properties),
|
||||
async (params: Record<string, unknown>) => {
|
||||
console.log(`Calling tool: ${tool.name} with params: ${JSON.stringify(params)}`);
|
||||
|
||||
|
||||
const result = await serverInfo.client!.callTool({
|
||||
name: tool.name,
|
||||
arguments: params,
|
||||
});
|
||||
|
||||
|
||||
console.log(`Tool result: ${JSON.stringify(result)}`);
|
||||
return result as CallToolResult;
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to connect to server for client: ${serverInfo.name} by error: ${error}`);
|
||||
console.error(
|
||||
`Failed to connect to server for client: ${serverInfo.name} by error: ${error}`,
|
||||
);
|
||||
serverInfo.status = 'disconnected';
|
||||
}
|
||||
}
|
||||
@@ -143,25 +148,23 @@ export const getServersInfo = (): Omit<ServerInfo, 'client' | 'transport'>[] =>
|
||||
export const addServer = async (
|
||||
mcpServer: McpServer,
|
||||
name: string,
|
||||
config: ServerConfig
|
||||
config: ServerConfig,
|
||||
): Promise<{ success: boolean; message?: string }> => {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
|
||||
|
||||
if (settings.mcpServers[name]) {
|
||||
return { success: false, message: 'Server name already exists' };
|
||||
}
|
||||
|
||||
|
||||
settings.mcpServers[name] = config;
|
||||
|
||||
|
||||
if (!saveSettings(settings)) {
|
||||
return { success: false, message: 'Failed to save settings' };
|
||||
}
|
||||
|
||||
// Reinitialize clients and register tools
|
||||
serverInfos = initializeClientsFromSettings();
|
||||
await registerAllTools(mcpServer);
|
||||
|
||||
|
||||
registerAllTools(mcpServer);
|
||||
|
||||
return { success: true, message: 'Server added successfully' };
|
||||
} catch (error) {
|
||||
console.error(`Failed to add server: ${name}`, error);
|
||||
@@ -173,27 +176,27 @@ export const addServer = async (
|
||||
export const removeServer = (name: string): { success: boolean; message?: string } => {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
|
||||
|
||||
if (!settings.mcpServers[name]) {
|
||||
return { success: false, message: 'Server not found' };
|
||||
}
|
||||
|
||||
|
||||
delete settings.mcpServers[name];
|
||||
|
||||
|
||||
if (!saveSettings(settings)) {
|
||||
return { success: false, message: 'Failed to save settings' };
|
||||
}
|
||||
|
||||
|
||||
// Close existing connections
|
||||
const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name);
|
||||
if (serverInfo && serverInfo.client) {
|
||||
serverInfo.client.close();
|
||||
serverInfo.transport?.close();
|
||||
}
|
||||
|
||||
|
||||
// Remove from list
|
||||
serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name);
|
||||
|
||||
|
||||
return { success: true, message: 'Server removed successfully' };
|
||||
} catch (error) {
|
||||
console.error(`Failed to remove server: ${name}`, error);
|
||||
@@ -211,18 +214,18 @@ function cast(inputSchema: unknown): ZodRawShape {
|
||||
if (typeof inputSchema !== 'object' || inputSchema === null) {
|
||||
throw new Error('Invalid input schema');
|
||||
}
|
||||
|
||||
|
||||
const properties = inputSchema as Record<string, { type: string; description?: string }>;
|
||||
const processedSchema: ZodRawShape = {};
|
||||
|
||||
|
||||
for (const key in properties) {
|
||||
const prop = properties[key];
|
||||
|
||||
|
||||
if (prop instanceof ZodType) {
|
||||
processedSchema[key] = prop.optional();
|
||||
} else if (typeof prop === 'object' && prop !== null) {
|
||||
let zodType: ZodType;
|
||||
|
||||
|
||||
switch (prop.type) {
|
||||
case 'string':
|
||||
zodType = z.string();
|
||||
@@ -245,14 +248,14 @@ function cast(inputSchema: unknown): ZodRawShape {
|
||||
default:
|
||||
zodType = z.any();
|
||||
}
|
||||
|
||||
|
||||
if (prop.description) {
|
||||
zodType = zodType.describe(prop.description);
|
||||
}
|
||||
|
||||
|
||||
processedSchema[key] = zodType.optional();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return processedSchema;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user