This commit is contained in:
samanhappy
2025-04-03 23:36:05 +08:00
parent ab73f2cb99
commit 3ebceefdef
11 changed files with 639 additions and 419 deletions

View File

@@ -1,4 +1,5 @@
function DeleteDialog({ isOpen, onClose, onConfirm, serverName }) {
// 定义DeleteDialog组件并将其暴露为全局变量
window.DeleteDialog = function DeleteDialog({ isOpen, onClose, onConfirm, serverName }) {
return (
<div className={`${isOpen ? 'block' : 'hidden'} fixed inset-0 bg-black bg-opacity-50 z-50`}>
<div className="fixed inset-0 flex items-center justify-center">
@@ -28,5 +29,3 @@ function DeleteDialog({ isOpen, onClose, onConfirm, serverName }) {
</div>
);
}
module.exports = DeleteDialog;

View File

@@ -421,14 +421,38 @@ function App() {
useEffect(() => {
fetch('/api/servers')
.then((response) => response.json())
.then((data) => setServers(data))
.then((data) => {
// 处理API响应中的包装对象提取data字段
if (data && data.success && Array.isArray(data.data)) {
setServers(data.data);
} else if (data && Array.isArray(data)) {
// 兼容性处理如果API直接返回数组
setServers(data);
} else {
// 如果数据格式不符合预期,设置为空数组
console.error('Invalid server data format:', data);
setServers([]);
}
})
.catch((err) => setError(err.message));
// Poll for updates every 5 seconds
const interval = setInterval(() => {
fetch('/api/servers')
.then((response) => response.json())
.then((data) => setServers(data))
.then((data) => {
// 处理API响应中的包装对象提取data字段
if (data && data.success && Array.isArray(data.data)) {
setServers(data.data);
} else if (data && Array.isArray(data)) {
// 兼容性处理如果API直接返回数组
setServers(data);
} else {
// 如果数据格式不符合预期,设置为空数组
console.error('Invalid server data format:', data);
setServers([]);
}
})
.catch((err) => setError(err.message));
}, 5000);

44
src/config/index.ts Normal file
View File

@@ -0,0 +1,44 @@
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs';
import { McpSettings } from '../types/index.js';
dotenv.config();
const defaultConfig = {
port: process.env.PORT || 3000,
mcpHubName: 'mcphub',
mcpHubVersion: '0.0.1',
};
export const getSettingsPath = (): string => {
return path.resolve(process.cwd(), 'mcp_settings.json');
};
export const loadSettings = (): McpSettings => {
const settingsPath = getSettingsPath();
try {
const settingsData = fs.readFileSync(settingsPath, 'utf8');
return JSON.parse(settingsData);
} catch (error) {
console.error(`Failed to load settings from ${settingsPath}:`, error);
return { mcpServers: {} };
}
};
export const saveSettings = (settings: McpSettings): boolean => {
const settingsPath = getSettingsPath();
try {
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
return true;
} catch (error) {
console.error(`Failed to save settings to ${settingsPath}:`, error);
return false;
}
};
export const expandEnvVars = (value: string): string => {
return value.replace(/\$\{([^}]+)\}/g, (_, key) => process.env[key] || '');
};
export default defaultConfig;

View File

@@ -0,0 +1,124 @@
import { Request, Response } from 'express';
import { ApiResponse, AddServerRequest } from '../types/index.js';
import { getServersInfo, addServer, removeServer } from '../services/mcpService.js';
import { loadSettings } from '../config/index.js';
let mcpServerInstance: any;
export const setMcpServerInstance = (server: any): void => {
mcpServerInstance = server;
};
export const getAllServers = (_: Request, res: Response): void => {
try {
const serversInfo = getServersInfo();
const response: ApiResponse = {
success: true,
data: serversInfo
};
res.json(response);
} catch (error) {
res.status(500).json({
success: false,
message: 'Failed to get servers information'
});
}
};
export const getAllSettings = (_: Request, res: Response): void => {
try {
const settings = loadSettings();
const response: ApiResponse = {
success: true,
data: settings
};
res.json(response);
} catch (error) {
res.status(500).json({
success: false,
message: 'Failed to get server settings'
});
}
};
export const createServer = async (req: Request, res: Response): Promise<void> => {
try {
const { name, config } = req.body as AddServerRequest;
if (!name || typeof name !== 'string') {
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 addServer(mcpServerInstance, name, config);
if (result.success) {
res.json({
success: true,
message: 'Server added successfully'
});
} else {
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'
});
}
};
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'
});
return;
}
const result = removeServer(name);
if (result.success) {
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'
});
}
} catch (error) {
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
};

View File

@@ -1,134 +1,17 @@
import express, { Request, Response } from 'express';
import dotenv from 'dotenv';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import {
registerAllTools,
getServersInfo,
getServersSettings,
addServer,
removeServer,
} from './server.js';
import path from 'path';
import AppServer from './server.js';
dotenv.config();
const appServer = new AppServer();
let server = new McpServer({
name: 'mcphub',
version: '0.0.1',
});
// Register all MCP tools from the modular structure
registerAllTools(server);
const app = express();
const PORT = process.env.PORT || 3000;
// Serve static files from the public directory
app.use(express.static('public'));
// Add conditional JSON parsing middleware
app.use((req, res, next) => {
if (req.path !== '/sse' && req.path !== '/messages') {
express.json()(req, res, next);
} else {
next();
async function boot() {
try {
await appServer.initialize();
appServer.start();
} catch (error) {
console.error('Failed to start application:', error);
process.exit(1);
}
});
}
// to support multiple simultaneous connections we have a lookup object from sessionId to transport
const transports: { [sessionId: string]: SSEServerTransport } = {};
boot();
// API endpoint to get server and tools information
app.get('/api/servers', (req: Request, res: Response) => {
const serversInfo = getServersInfo();
res.json(serversInfo);
});
// API endpoint to get all server settings
app.get('/api/settings', (req: Request, res: Response) => {
const settings = getServersSettings();
res.json(settings);
});
// API endpoint to add a new server
app.post('/api/servers', async (req: Request, res: Response) => {
const { name, config } = req.body;
if (!name || typeof name !== 'string') {
return res.status(400).json({ success: false, message: 'Server name is required' });
}
if (!config || typeof config !== 'object') {
return res.status(400).json({ success: false, message: 'Server configuration is required' });
}
// Validate config has either url or command+args
if (!config.url && (!config.command || !config.args)) {
return res.status(400).json({
success: false,
message: 'Server configuration must include either a URL or command with arguments',
});
}
const { success, message } = await addServer(server, name, config);
if (success) {
res.json({ success: true, message: 'Server added successfully' });
} else {
res.status(400).json({ success: false, message: message || 'Failed to add server' });
}
});
// API endpoint to remove a server
app.delete('/api/servers/:name', async (req: Request, res: Response) => {
const { name } = req.params;
if (!name) {
return res.status(400).json({ success: false, message: 'Server name is required' });
}
const result = removeServer(name);
if (result.success) {
server = new McpServer({
name: 'mcphub',
version: '0.0.1',
});
registerAllTools(server);
res.json({ success: true, message: 'Server removed successfully' });
} else {
res.status(404).json({ success: false, message: 'Server not found or failed to remove' });
}
});
app.get('/sse', async (_: Request, res: Response) => {
const transport = new SSEServerTransport('/messages', res);
transports[transport.sessionId] = transport;
res.on('close', () => {
delete transports[transport.sessionId];
});
await server.connect(transport);
});
// Serve index.html for the root route
app.get('/', (req: Request, res: Response) => {
res.sendFile(path.join(process.cwd(), 'public', 'index.html'));
});
app.post('/messages', async (req: Request, res: Response) => {
const sessionId = req.query.sessionId as string;
const transport = transports[sessionId];
if (transport) {
await transport.handlePostMessage(req, res);
} else {
console.error(`No transport found for sessionId: ${sessionId}`);
res.status(400).send('No transport found for sessionId');
}
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
export default app;
export default appServer.getApp();

33
src/middlewares/index.ts Normal file
View File

@@ -0,0 +1,33 @@
import express, { Request, Response, NextFunction } from 'express';
import path from 'path';
export const errorHandler = (
err: Error,
_req: Request,
res: Response,
_next: NextFunction
): void => {
console.error('Unhandled error:', err);
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);
} else {
next();
}
});
app.get('/', (_req: Request, res: Response) => {
res.sendFile(path.join(process.cwd(), 'public', 'index.html'));
});
app.use(errorHandler);
};

24
src/routes/index.ts Normal file
View File

@@ -0,0 +1,24 @@
import express from 'express';
import {
getAllServers,
getAllSettings,
createServer,
deleteServer,
setMcpServerInstance
} from '../controllers/serverController.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
const router = express.Router();
export const initRoutes = (app: express.Application, server: McpServer): void => {
setMcpServerInstance(server);
router.get('/servers', getAllServers);
router.get('/settings', getAllSettings);
router.post('/servers', createServer);
router.delete('/servers/:name', deleteServer);
app.use('/api', router);
};
export default router;

View File

@@ -1,300 +1,49 @@
import express from 'express';
import config from './config/index.js';
import { createMcpServer, registerAllTools } from './services/mcpService.js';
import { initMiddlewares } from './middlewares/index.js';
import { initRoutes } from './routes/index.js';
import { handleSseConnection, handleSseMessage } from './services/sseService.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import * as z from 'zod';
import { ZodType, ZodRawShape } from 'zod';
import fs from 'fs';
import path from 'path';
// Define settings interface
interface McpSettings {
mcpServers: {
[key: string]: {
url?: string;
command?: string;
args?: string[];
env?: Record<string, string>;
};
};
}
export class AppServer {
private app: express.Application;
private mcpServer: McpServer;
private port: number | string;
// Add type definitions for API responses
export interface ServerInfo {
name: string;
status: 'connected' | 'connecting' | 'disconnected';
tools: ToolInfo[];
client?: Client;
transport?: SSEClientTransport | StdioClientTransport;
}
export interface ToolInfo {
name: string;
description: string;
inputSchema: Record<string, unknown>;
}
// Function to read and parse the settings file
function loadSettings(): McpSettings {
const settingsPath = path.resolve(process.cwd(), 'mcp_settings.json');
try {
const settingsData = fs.readFileSync(settingsPath, 'utf8');
return JSON.parse(settingsData);
} catch (error) {
console.error(`Failed to load settings from ${settingsPath}:`, error);
return { mcpServers: {} };
}
}
// Function to save settings to file
export function saveSettings(settings: McpSettings): boolean {
const settingsPath = path.resolve(process.cwd(), 'mcp_settings.json');
try {
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
return true;
} catch (error) {
console.error(`Failed to save settings to ${settingsPath}:`, error);
return false;
}
}
// Initialize clients and transports from settings
function initializeClientsFromSettings(): ServerInfo[] {
const settings = loadSettings();
function expandEnvVars(value: string) {
return value.replace(/\$\{([^}]+)\}/g, (_, key) => process.env[key] || '');
constructor() {
this.app = express();
this.port = config.port;
this.mcpServer = createMcpServer(config.mcpHubName, config.mcpHubVersion);
}
for (const [name, config] of Object.entries(settings.mcpServers)) {
const serverInfo = serverInfos.find((info) => info.name === name);
if (serverInfo && serverInfo.status === 'connected') {
console.log(`Server '${name}' is already connected.`);
continue;
}
let transport;
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);
}
}
transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: env,
});
} else {
console.warn(`Skipping server '${name}': missing required configuration`);
serverInfos.push({
name,
status: 'disconnected',
tools: [],
});
continue;
}
const client = new Client(
{
name: `mcp-client-${name}`,
version: '1.0.0',
},
{
capabilities: {
prompts: {},
resources: {},
tools: {},
},
},
);
serverInfos.push({
name,
status: 'connecting', // Set to connecting when client exists
tools: [],
client,
transport,
});
console.log(`Initialized client for server: ${name}`);
}
return serverInfos;
}
// Initialize server info
let serverInfos: ServerInfo[] = [];
serverInfos = initializeClientsFromSettings();
// Export the registerAllTools function
export const registerAllTools = async (server: McpServer) => {
for (const serverInfo of serverInfos) {
if (serverInfo.status === 'connected') continue;
if (!serverInfo.client || !serverInfo.transport) continue;
async initialize(): 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;
},
);
}
await registerAllTools(this.mcpServer);
initMiddlewares(this.app);
initRoutes(this.app, this.mcpServer);
this.app.get('/sse', (req, res) => handleSseConnection(req, res, this.mcpServer));
this.app.post('/messages', handleSseMessage);
console.log('Server initialized successfully');
} catch (error) {
console.error(
`Failed to connect to server for client: ${serverInfo.name} by error: ${error}`,
);
serverInfo.status = 'disconnected';
console.error('Error initializing server:', error);
throw error;
}
}
};
// Add function to get current server status
export function getServersInfo(): ServerInfo[] {
return serverInfos.map(({ name, status, tools }) => ({
name,
status,
tools,
}));
}
start(): void {
this.app.listen(this.port, () => {
console.log(`Server is running on port ${this.port}`);
});
}
// Add function to get all server settings
export function getServersSettings(): McpSettings {
return loadSettings();
}
getApp(): express.Application {
return this.app;
}
// Add function to add a new server
export async function addServer(
mcpServer: McpServer,
name: string,
config: { url?: string; command?: string; args?: string[]; env?: Record<string, string> },
): 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' };
}
serverInfos = initializeClientsFromSettings();
registerAllTools(mcpServer);
return { success: true, message: 'Server added successfully' };
} catch (error) {
console.error(`Failed to add server: ${name}`, error);
return { success: false, message: 'Failed to add server' };
getMcpServer(): McpServer {
return this.mcpServer;
}
}
export function removeServer(name: string): {
success: boolean;
} {
try {
const settings = loadSettings();
if (!settings.mcpServers[name]) {
return { success: false };
}
delete settings.mcpServers[name];
if (!saveSettings(settings)) {
return { success: false };
}
const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name);
if (serverInfo && serverInfo.client) {
serverInfo.client.close();
serverInfo.transport?.close();
}
serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name);
return { success: true };
} catch (error) {
console.error(`Failed to remove server: ${name}`, error);
return { success: false };
}
}
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();
break;
case 'number':
zodType = z.number();
break;
case 'boolean':
zodType = z.boolean();
break;
case 'integer':
zodType = z.number().int();
break;
case 'array':
zodType = z.array(z.any());
break;
case 'object':
zodType = z.record(z.any());
break;
default:
zodType = z.any();
}
if (prop.description) {
zodType = zodType.describe(prop.description);
}
processedSchema[key] = zodType.optional();
}
}
return processedSchema;
}
export default AppServer;

258
src/services/mcpService.ts Normal file
View File

@@ -0,0 +1,258 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
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 { loadSettings, saveSettings, expandEnvVars } from '../config/index.js';
// Store all server information
let serverInfos: ServerInfo[] = [];
// Initialize MCP server clients
export const initializeClientsFromSettings = (): ServerInfo[] => {
const settings = loadSettings();
const existingServerInfos = serverInfos;
serverInfos = [];
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');
if (existingServer) {
serverInfos.push(existingServer);
console.log(`Server '${name}' is already connected.`);
continue;
}
let transport;
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);
}
}
transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: env,
});
} else {
console.warn(`Skipping server '${name}': missing required configuration`);
serverInfos.push({
name,
status: 'disconnected',
tools: [],
});
continue;
}
const client = new Client(
{
name: `mcp-client-${name}`,
version: '1.0.0',
},
{
capabilities: {
prompts: {},
resources: {},
tools: {},
},
},
);
serverInfos.push({
name,
status: 'connecting',
tools: [],
client,
transport,
});
console.log(`Initialized client for server: ${name}`);
}
return serverInfos;
};
// Register all MCP tools
export const registerAllTools = async (server: McpServer): Promise<void> => {
for (const serverInfo of serverInfos) {
if (serverInfo.status === 'connected') continue;
if (!serverInfo.client || !serverInfo.transport) continue;
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}`);
serverInfo.status = 'disconnected';
}
}
};
// Get all server information
export const getServersInfo = (): Omit<ServerInfo, 'client' | 'transport'>[] => {
return serverInfos.map(({ name, status, tools }) => ({
name,
status,
tools,
}));
};
// Add new server
export const addServer = 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 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);
return { success: true, message: 'Server added successfully' };
} catch (error) {
console.error(`Failed to add server: ${name}`, error);
return { success: false, message: 'Failed to add server' };
}
};
// Remove server
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);
return { success: false, message: `Failed to remove server: ${error}` };
}
};
// Create McpServer instance
export const createMcpServer = (name: string, version: string): McpServer => {
return new McpServer({ name, version });
};
// Helper function: Convert JSON Schema to Zod Schema
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();
break;
case 'number':
zodType = z.number();
break;
case 'boolean':
zodType = z.boolean();
break;
case 'integer':
zodType = z.number().int();
break;
case 'array':
zodType = z.array(z.any());
break;
case 'object':
zodType = z.record(z.any());
break;
default:
zodType = z.any();
}
if (prop.description) {
zodType = zodType.describe(prop.description);
}
processedSchema[key] = zodType.optional();
}
}
return processedSchema;
}

View File

@@ -0,0 +1,41 @@
import { Request, Response } from 'express';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
const transports: { [sessionId: string]: SSEServerTransport } = {};
export const handleSseConnection = async (
req: Request,
res: Response,
server: McpServer
): Promise<void> => {
const transport = new SSEServerTransport('/messages', res);
transports[transport.sessionId] = transport;
res.on('close', () => {
delete transports[transport.sessionId];
console.log(`SSE connection closed: ${transport.sessionId}`);
});
console.log(`New SSE connection established: ${transport.sessionId}`);
await server.connect(transport);
};
export const handleSseMessage = async (
req: Request,
res: Response
): Promise<void> => {
const sessionId = req.query.sessionId as string;
const transport = transports[sessionId];
if (transport) {
await transport.handlePostMessage(req, res);
} else {
console.error(`No transport found for sessionId: ${sessionId}`);
res.status(400).send('No transport found for sessionId');
}
};
export const getConnectionCount = (): number => {
return Object.keys(transports).length;
};

41
src/types/index.ts Normal file
View File

@@ -0,0 +1,41 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
export interface McpSettings {
mcpServers: {
[key: string]: ServerConfig;
};
}
export interface ServerConfig {
url?: string;
command?: string;
args?: string[];
env?: Record<string, string>;
}
export interface ServerInfo {
name: string;
status: 'connected' | 'connecting' | 'disconnected';
tools: ToolInfo[];
client?: Client;
transport?: SSEClientTransport | StdioClientTransport;
}
export interface ToolInfo {
name: string;
description: string;
inputSchema: Record<string, unknown>;
}
export interface ApiResponse<T = any> {
success: boolean;
message?: string;
data?: T;
}
export interface AddServerRequest {
name: string;
config: ServerConfig;
}