feat: create MCP server for different session (#74)

This commit is contained in:
samanhappy
2025-05-12 15:11:37 +08:00
committed by GitHub
parent 5d798cfe6a
commit f1a5f692cc
5 changed files with 103 additions and 90 deletions

View File

@@ -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': {

View File

@@ -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
View File

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

View File

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

View File

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