mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
feat: create MCP server for different session (#74)
This commit is contained in:
@@ -6,9 +6,7 @@ import tailwindcss from '@tailwindcss/vite';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
// Get package.json version
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8')
|
||||
);
|
||||
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -22,6 +20,9 @@ export default defineConfig({
|
||||
// Make package version available as global variable
|
||||
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
|
||||
},
|
||||
build: {
|
||||
sourcemap: true, // Enable source maps for production build
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"backend:build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"backend:dev": "tsx watch src/index.ts",
|
||||
"backend:debug": "tsx watch src/index.ts --inspect",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"test": "jest",
|
||||
@@ -28,6 +29,7 @@
|
||||
"frontend:build": "cd frontend && vite build",
|
||||
"frontend:preview": "cd frontend && vite preview",
|
||||
"dev": "concurrently \"pnpm backend:dev\" \"pnpm frontend:dev\"",
|
||||
"debug": "concurrently \"pnpm backend:debug\" \"pnpm frontend:dev\"",
|
||||
"prepublishOnly": "npm run build && node scripts/verify-dist.js"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -39,7 +41,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@modelcontextprotocol/sdk": "^1.11.1",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
@@ -94,4 +96,4 @@
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha256.fa0f513aa8191764d2b6b432420788c270f07b4f999099b65bb2010eec702a30"
|
||||
}
|
||||
}
|
||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: ^1.10.2
|
||||
version: 1.10.2
|
||||
specifier: ^1.11.1
|
||||
version: 1.11.1
|
||||
bcryptjs:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
@@ -867,8 +867,8 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.10.2':
|
||||
resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==}
|
||||
'@modelcontextprotocol/sdk@1.11.1':
|
||||
resolution: {integrity: sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@next/env@15.2.4':
|
||||
@@ -4263,7 +4263,7 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@modelcontextprotocol/sdk@1.10.2':
|
||||
'@modelcontextprotocol/sdk@1.11.1':
|
||||
dependencies:
|
||||
content-type: 1.0.5
|
||||
cors: 2.8.5
|
||||
@@ -6781,7 +6781,7 @@ snapshots:
|
||||
|
||||
send@1.2.0:
|
||||
dependencies:
|
||||
debug: 4.3.6
|
||||
debug: 4.4.0
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
|
||||
@@ -9,31 +9,36 @@ import config from '../config/index.js';
|
||||
import { getGroup } from './sseService.js';
|
||||
import { getServersInGroup } from './groupService.js';
|
||||
|
||||
let currentServer: Server;
|
||||
const servers: { [sessionId: string]: Server } = {};
|
||||
|
||||
export const initMcpServer = async (name: string, version: string): Promise<void> => {
|
||||
currentServer = createMcpServer(name, version);
|
||||
await registerAllTools(currentServer, true, true);
|
||||
await registerAllTools(true);
|
||||
};
|
||||
|
||||
export const setMcpServer = (server: Server): void => {
|
||||
currentServer = server;
|
||||
export const getMcpServer = (sessionId: string): Server => {
|
||||
if (!servers[sessionId]) {
|
||||
const server = createMcpServer(config.mcpHubName, config.mcpHubVersion);
|
||||
servers[sessionId] = server;
|
||||
}
|
||||
return servers[sessionId];
|
||||
};
|
||||
|
||||
export const getMcpServer = (): Server => {
|
||||
return currentServer;
|
||||
export const deleteMcpServer = (sessionId: string): void => {
|
||||
delete servers[sessionId];
|
||||
};
|
||||
|
||||
export const notifyToolChanged = async () => {
|
||||
await registerAllTools(currentServer, true, false);
|
||||
currentServer
|
||||
.sendToolListChanged()
|
||||
.catch((error) => {
|
||||
console.warn('Failed to send tool list changed notification:', error.message);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Tool list changed notification sent successfully');
|
||||
});
|
||||
await registerAllTools(false);
|
||||
Object.values(servers).forEach((server) => {
|
||||
server
|
||||
.sendToolListChanged()
|
||||
.catch((error) => {
|
||||
console.warn('Failed to send tool list changed notification:', error.message);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Tool list changed notification sent successfully');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Store all server information
|
||||
@@ -79,13 +84,13 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
|
||||
} else if (conf.command && conf.args) {
|
||||
const env: Record<string, string> = conf.env || {};
|
||||
env['PATH'] = expandEnvVars(process.env.PATH as string) || '';
|
||||
|
||||
|
||||
// Add UV_DEFAULT_INDEX from settings if available (for Python packages)
|
||||
const settings = loadSettings();
|
||||
if (settings.systemConfig?.install?.pythonIndexUrl && conf.command === 'uvx') {
|
||||
env['UV_DEFAULT_INDEX'] = settings.systemConfig.install.pythonIndexUrl;
|
||||
}
|
||||
|
||||
|
||||
transport = new StdioClientTransport({
|
||||
command: conf.command,
|
||||
args: conf.args,
|
||||
@@ -180,11 +185,7 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
|
||||
};
|
||||
|
||||
// Register all MCP tools
|
||||
export const registerAllTools = async (
|
||||
server: Server,
|
||||
forceInit: boolean,
|
||||
isInit: boolean,
|
||||
): Promise<void> => {
|
||||
export const registerAllTools = async (isInit: boolean): Promise<void> => {
|
||||
initializeClientsFromSettings(isInit);
|
||||
};
|
||||
|
||||
@@ -236,7 +237,7 @@ export const addServer = async (
|
||||
return { success: false, message: 'Failed to save settings' };
|
||||
}
|
||||
|
||||
registerAllTools(currentServer, false, false);
|
||||
registerAllTools(false);
|
||||
return { success: true, message: 'Server added successfully' };
|
||||
} catch (error) {
|
||||
console.error(`Failed to add server: ${name}`, error);
|
||||
@@ -343,59 +344,62 @@ export const toggleServerStatus = async (
|
||||
}
|
||||
};
|
||||
|
||||
const handleListToolsRequest = async (_: any, extra: any) => {
|
||||
const sessionId = extra.sessionId || '';
|
||||
const group = getGroup(sessionId);
|
||||
console.log(`Handling ListToolsRequest for group: ${group}`);
|
||||
const allServerInfos = serverInfos.filter((serverInfo) => {
|
||||
if (serverInfo.enabled === false) return false;
|
||||
if (!group) return true;
|
||||
const serversInGroup = getServersInGroup(group);
|
||||
if (!serversInGroup || serversInGroup.length === 0) return serverInfo.name === group;
|
||||
return serversInGroup.includes(serverInfo.name);
|
||||
});
|
||||
|
||||
const allTools = [];
|
||||
for (const serverInfo of allServerInfos) {
|
||||
if (serverInfo.tools && serverInfo.tools.length > 0) {
|
||||
allTools.push(...serverInfo.tools);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tools: allTools,
|
||||
};
|
||||
};
|
||||
|
||||
const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
console.log(`Handling CallToolRequest for tool: ${request.params.name}`);
|
||||
try {
|
||||
const serverInfo = getServerByTool(request.params.name);
|
||||
if (!serverInfo) {
|
||||
throw new Error(`Server not found: ${request.params.name}`);
|
||||
}
|
||||
const client = serverInfo.client;
|
||||
if (!client) {
|
||||
throw new Error(`Client not found for server: ${request.params.name}`);
|
||||
}
|
||||
const result = await client.callTool(request.params);
|
||||
console.log(`Tool call result: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error handling CallToolRequest: ${error}`);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Create McpServer instance
|
||||
export const createMcpServer = (name: string, version: string): Server => {
|
||||
const server = new Server({ name, version }, { capabilities: { tools: {} } });
|
||||
server.setRequestHandler(ListToolsRequestSchema, async (_, extra) => {
|
||||
const sessionId = extra.sessionId || '';
|
||||
const group = getGroup(sessionId);
|
||||
console.log(`Handling ListToolsRequest for group: ${group}`);
|
||||
const allServerInfos = serverInfos.filter((serverInfo) => {
|
||||
if (serverInfo.enabled === false) return false;
|
||||
if (!group) return true;
|
||||
const serversInGroup = getServersInGroup(group);
|
||||
if (!serversInGroup || serversInGroup.length === 0) return serverInfo.name === group;
|
||||
return serversInGroup.includes(serverInfo.name);
|
||||
});
|
||||
|
||||
const allTools = [];
|
||||
for (const serverInfo of allServerInfos) {
|
||||
if (serverInfo.tools && serverInfo.tools.length > 0) {
|
||||
allTools.push(...serverInfo.tools);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tools: allTools,
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request, _) => {
|
||||
console.log(`Handling CallToolRequest for tool: ${request.params.name}`);
|
||||
try {
|
||||
const serverInfo = getServerByTool(request.params.name);
|
||||
if (!serverInfo) {
|
||||
throw new Error(`Server not found: ${request.params.name}`);
|
||||
}
|
||||
const client = serverInfo.client;
|
||||
if (!client) {
|
||||
throw new Error(`Client not found for server: ${request.params.name}`);
|
||||
}
|
||||
const result = await client.callTool(request.params);
|
||||
console.log(`Tool call result: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error handling CallToolRequest: ${error}`);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
server.setRequestHandler(ListToolsRequestSchema, handleListToolsRequest);
|
||||
server.setRequestHandler(CallToolRequestSchema, handleCallToolRequest);
|
||||
return server;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { getMcpServer } from './mcpService.js';
|
||||
import { deleteMcpServer, getMcpServer } from './mcpService.js';
|
||||
import { loadSettings } from '../config/index.js';
|
||||
|
||||
const transports: { [sessionId: string]: { transport: Transport; group: string } } = {};
|
||||
@@ -32,13 +32,14 @@ export const handleSseConnection = async (req: Request, res: Response): Promise<
|
||||
|
||||
res.on('close', () => {
|
||||
delete transports[transport.sessionId];
|
||||
deleteMcpServer(transport.sessionId);
|
||||
console.log(`SSE connection closed: ${transport.sessionId}`);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`New SSE connection established: ${transport.sessionId} with group: ${group || 'global'}`,
|
||||
);
|
||||
await getMcpServer().connect(transport);
|
||||
await getMcpServer(transport.sessionId).connect(transport);
|
||||
};
|
||||
|
||||
export const handleSseMessage = async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -56,9 +57,9 @@ export const handleSseMessage = async (req: Request, res: Response): Promise<voi
|
||||
};
|
||||
|
||||
export const handleMcpPostRequest = async (req: Request, res: Response): Promise<void> => {
|
||||
console.log('Handling MCP post request');
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
const group = req.params.group;
|
||||
console.log(`Handling MCP post request for sessionId: ${sessionId} and group: ${group}`);
|
||||
const settings = loadSettings();
|
||||
const routingConfig = settings.systemConfig?.routing || {
|
||||
enableGlobalRoute: true,
|
||||
@@ -71,6 +72,7 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise
|
||||
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
if (sessionId && transports[sessionId]) {
|
||||
console.log(`Reusing existing transport for sessionId: ${sessionId}`);
|
||||
transport = transports[sessionId].transport as StreamableHTTPServerTransport;
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
@@ -83,10 +85,13 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise
|
||||
transport.onclose = () => {
|
||||
if (transport.sessionId) {
|
||||
delete transports[transport.sessionId];
|
||||
deleteMcpServer(transport.sessionId);
|
||||
console.log(`MCP connection closed: ${transport.sessionId}`);
|
||||
}
|
||||
};
|
||||
|
||||
await getMcpServer().connect(transport);
|
||||
console.log(`MCP connection established: ${transport.sessionId}`);
|
||||
await getMcpServer(transport.sessionId || 'mcp').connect(transport);
|
||||
} else {
|
||||
res.status(400).json({
|
||||
jsonrpc: '2.0',
|
||||
@@ -99,6 +104,7 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Handling request using transport with type ${transport.constructor.name}`);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user