UNPKG

feishu-mcp

Version:

Model Context Protocol server for Feishu integration

312 lines (311 loc) 15 kB
import express from 'express'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { randomUUID } from 'node:crypto'; import { Logger } from './utils/logger.js'; import { SSEConnectionManager } from './manager/sseConnectionManager.js'; import { FeishuMcp } from './mcp/feishuMcp.js'; import { callback } from './services/callbackService.js'; import { UserAuthManager, UserContextManager, getBaseUrl, TokenCacheManager, TokenRefreshManager } from './utils/auth/index.js'; export class FeishuMcpServer { constructor() { Object.defineProperty(this, "connectionManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "userAuthManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "userContextManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "callbackServer", { enumerable: true, configurable: true, writable: true, value: null }); // stdio 模式下的 callback 服务器实例 this.connectionManager = new SSEConnectionManager(); this.userAuthManager = UserAuthManager.getInstance(); this.userContextManager = UserContextManager.getInstance(); // 初始化TokenCacheManager,确保在启动时从文件加载缓存 TokenCacheManager.getInstance(); // 启动Token自动刷新管理器 const tokenRefreshManager = TokenRefreshManager.getInstance(); tokenRefreshManager.start(); Logger.info('Token自动刷新管理器已在服务器启动时初始化'); } async connect(transport) { const server = new FeishuMcp(); await server.connect(transport); const shutdown = () => { TokenRefreshManager.getInstance().stop(); this.stopCallbackServer(); process.exit(0); }; // 监听 transport 关闭事件,清理 callback 服务器 // 对于 stdio 模式,监听 stdin 关闭事件 if (process.stdin && typeof process.stdin.on === 'function') { process.stdin.on('close', shutdown); process.stdin.on('end', shutdown); } // 监听进程退出事件,确保清理资源 process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); // 注意:在 stdio 模式下,Logger 会自动禁用输出,避免污染 MCP 协议 // 如果需要日志,可以通过 MCP 协议的 logging 消息传递 Logger.info('Server connected and ready to process requests'); } /** * 停止 callback 服务器 */ stopCallbackServer() { if (this.callbackServer) { this.callbackServer.close(() => { Logger.info('Callback server stopped'); }); this.callbackServer = null; } } async startHttpServer(port) { const app = express(); const transports = {}; // Parse JSON requests for the Streamable HTTP endpoint only, will break SSE endpoint app.use("/mcp", express.json()); app.post('/mcp', async (req, res) => { try { Logger.log("Received StreamableHTTP request", { method: req.method, url: req.url, headers: req.headers, body: req.body, query: req.query, params: req.params }); // Check for existing session ID const sessionId = req.headers['mcp-session-id']; // 优先使用客户端显式传入的 ?userKey=,若不传则在 session 初始化时 fallback 到 sessionId const queryUserKey = req.query.userKey; let transport; let userKey; if (sessionId && transports[sessionId]) { // Reuse existing transport,从 userAuthManager 取回持久化的 userKey Logger.log("Reusing existing StreamableHTTP transport for sessionId", sessionId); transport = transports[sessionId]; userKey = this.userAuthManager.getUserKeyBySessionId(sessionId) || sessionId; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId) => { // 建立 sessionId → userKey 持久映射:优先用客户端传入的,否则用 sessionId 兜底 const resolvedUserKey = queryUserKey || newSessionId; this.userAuthManager.createSession(newSessionId, resolvedUserKey); Logger.log(`[StreamableHTTP connection] ${newSessionId}, userKey: ${resolvedUserKey}`); transports[newSessionId] = transport; } }); // Clean up transport and server when closed transport.onclose = () => { if (transport.sessionId) { Logger.log(`[StreamableHTTP delete] ${transport.sessionId}`); this.userAuthManager.removeSession(transport.sessionId); delete transports[transport.sessionId]; } }; // Create and connect server instance const server = new FeishuMcp(); await server.connect(transport); // 初始化握手阶段 sessionId 尚未分配,使用 queryUserKey(若有)或临时占位符 userKey = queryUserKey || 'http-client'; } else { // Invalid request res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: null, }); return; } // 获取 baseUrl 并在用户上下文中处理请求 const baseUrl = getBaseUrl(req); // Handle the request with user context await this.userContextManager.run({ userKey, baseUrl }, async () => { await transport.handleRequest(req, res, req.body); }); } catch (error) { Logger.error('Error handling MCP request:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); // Handle GET requests for server-to-client notifications via Streamable HTTP app.get('/mcp', async (req, res) => { try { Logger.log("Received StreamableHTTP request get"); const sessionId = req.headers['mcp-session-id']; if (!sessionId || !transports[sessionId]) { res.status(400).send('Invalid or missing session ID'); return; } const transport = transports[sessionId]; // 获取 baseUrl 并在用户上下文中处理请求 const baseUrl = getBaseUrl(req); const userKey = this.userAuthManager.getUserKeyBySessionId(sessionId) || sessionId; await this.userContextManager.run({ userKey, baseUrl }, async () => { await transport.handleRequest(req, res); }); } catch (error) { Logger.error('Error handling GET request:', error); if (!res.headersSent) { res.status(500).send('Internal server error'); } } }); // Handle DELETE requests for session termination app.delete('/mcp', async (req, res) => { try { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !transports[sessionId]) { res.status(400).send('Invalid or missing session ID'); return; } const transport = transports[sessionId]; // 获取 baseUrl 并在用户上下文中处理请求 const baseUrl = getBaseUrl(req); const userKey = this.userAuthManager.getUserKeyBySessionId(sessionId) || sessionId; await this.userContextManager.run({ userKey, baseUrl }, async () => { await transport.handleRequest(req, res); }); // Clean up resources after session termination if (transport.sessionId) { this.userAuthManager.removeSession(transport.sessionId); delete transports[transport.sessionId]; } } catch (error) { Logger.error('Error handling DELETE request:', error); if (!res.headersSent) { res.status(500).send('Internal server error'); } } }); app.get('/sse', async (req, res) => { // 获取 userKey 参数 let userKey = req.query.userKey; const sseTransport = new SSEServerTransport('/messages', res); const sessionId = sseTransport.sessionId; // 如果 userKey 为空,使用 sessionId 替代 if (!userKey) { userKey = sessionId; } Logger.log(`[SSE Connection] New SSE connection established for sessionId ${sessionId}, userKey: ${userKey}, params:${JSON.stringify(req.params)} headers:${JSON.stringify(req.headers)} `); // 创建用户会话映射 this.userAuthManager.createSession(sessionId, userKey); Logger.log(`[UserAuth] Created session mapping: sessionId=${sessionId}, userKey=${userKey}`); this.connectionManager.addConnection(sessionId, sseTransport, req, res); try { const tempServer = new FeishuMcp(); await tempServer.connect(sseTransport); Logger.info(`[SSE Connection] Successfully connected transport for: ${sessionId}`); } catch (error) { Logger.error(`[SSE Connection] Error connecting server to transport for ${sessionId}:`, error); this.connectionManager.removeConnection(sessionId); // 清理用户会话映射 this.userAuthManager.removeSession(sessionId); if (!res.writableEnded) { res.status(500).end('Failed to connect MCP server to transport'); } return; } }); app.post('/messages', async (req, res) => { const sessionId = req.query.sessionId; // 通过 sessionId 获取 userKey const userKey = this.userAuthManager.getUserKeyBySessionId(sessionId); Logger.info(`[SSE messages] Received message with sessionId: ${sessionId}, userKey: ${userKey}, params: ${JSON.stringify(req.query)}, body: ${JSON.stringify(req.body)}`); if (!sessionId) { res.status(400).send('Missing sessionId query parameter'); return; } const transport = this.connectionManager.getTransport(sessionId); Logger.log(`[SSE messages] Retrieved transport for sessionId ${sessionId}: ${transport ? transport.sessionId : 'Transport not found'}`); if (!transport) { res .status(404) .send(`No active connection found for sessionId: ${sessionId}`); return; } // 获取 baseUrl const baseUrl = getBaseUrl(req); // 在用户上下文中执行 transport.handlePostMessage this.userContextManager.run({ userKey: userKey || '', baseUrl: baseUrl }, async () => { await transport.handlePostMessage(req, res); }); }); app.get('/callback', callback); app.listen(port, '0.0.0.0', () => { Logger.info(`HTTP server listening on port ${port}`); Logger.info(`SSE endpoint available at http://localhost:${port}/sse`); Logger.info(`Message endpoint available at http://localhost:${port}/messages`); Logger.info(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`); }); } /** * 启动最小化的HTTP服务器(仅提供callback接口) * 用于stdio模式下提供OAuth回调功能 * @param port 服务器端口 */ async startCallbackServer(port) { const app = express(); // 只注册callback接口 app.get('/callback', callback); return new Promise((resolve, reject) => { const server = app.listen(port, '0.0.0.0', () => { this.callbackServer = server; Logger.info(`Callback server listening on port ${port}`); Logger.info(`Callback endpoint available at http://localhost:${port}/callback`); resolve(); }); server.on('error', (err) => { if (err.code === 'EADDRINUSE') { // 端口被占用,说明其他进程已经启动了 callback 服务器 // 这是正常的,静默处理即可(多个 stdio 进程共享同一个 callback 服务器) Logger.debug(`Port ${port} is already in use, callback server may already be running`); resolve(); // 不抛出错误,因为 callback 服务器已经存在 } else { reject(err); } }); }); } }