UNPKG

@channel.io/be-user-chat-mcp

Version:

Backend UserChat Model Context Protocol server for Channel.io

200 lines 7.72 kB
#!/usr/bin/env node import { randomUUID } from "node:crypto"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import express from "express"; import { createContextFromHeaders, } from "./config/index.js"; import { registerSentryTool } from "./tools/sentry.tool.js"; import { registerUserChatTool } from "./tools/user-chat.tool.js"; import { registerWorkflowTool } from "./tools/workflow.tool.js"; const app = express(); // Enhanced Express middleware setup for MCP app.use(express.json({ limit: "10mb" })); app.use(express.raw({ type: "*/*", limit: "10mb" })); // CORS headers for MCP app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id"); res.header("Access-Control-Expose-Headers", "Mcp-Session-Id"); if (req.method === "OPTIONS") { return res.status(200).end(); } next(); }); // Map to store transports and contexts by session ID const transports = {}; const sessionContexts = {}; // Create and configure MCP server with context function createMcpServer(context) { const server = new McpServer({ name: "Channel.io BE UserChat MCP", version: "0.0.0", }); // context를 제공하는 함수를 전달 const getContext = () => context; registerUserChatTool(server, getContext); registerWorkflowTool(server, getContext); registerSentryTool(server, getContext); return server; } // Handle POST requests for client-to-server communication app.post("/mcp", (req, res) => { (async () => { try { // Check for existing session ID const sessionId = req.headers["mcp-session-id"]; let transport; let context; if (sessionId && transports[sessionId]) { // Reuse existing transport and context transport = transports[sessionId]; context = sessionContexts[sessionId]; // Handle the request with existing transport await transport.handleRequest(req, res, req.body); return; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request - create context from headers try { context = createContextFromHeaders(req.headers); } catch (error) { // Invalid headers res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: `Bad Request: ${error instanceof Error ? error.message : String(error)}`, }, id: null, }); return; } // Create new transport with proper configuration transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), enableJsonResponse: true, onsessioninitialized: (actualSessionId) => { // Store the transport and context by session ID transports[actualSessionId] = transport; sessionContexts[actualSessionId] = context; console.error(`New MCP session initialized: ${actualSessionId}`); }, }); // Clean up transport and context when closed transport.onclose = () => { if (transport.sessionId) { delete transports[transport.sessionId]; delete sessionContexts[transport.sessionId]; console.error(`MCP session closed: ${transport.sessionId}`); } }; // Create and connect server to transport const server = createMcpServer(context); await server.connect(transport); // Handle the initialization request await transport.handleRequest(req, res, req.body); return; } else { // Invalid request res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request: No valid session ID provided", }, id: null, }); return; } } catch (error) { console.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, }); } } })().catch((error) => { console.error("Unhandled error in POST /mcp:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error", }, id: null, }); } }); }); // Reusable handler for GET and DELETE requests const handleSessionRequest = (req, res) => { (async () => { const sessionId = req.headers["mcp-session-id"]; if (!sessionId || !transports[sessionId]) { res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Invalid or missing session ID", }, id: null, }); return; } const transport = transports[sessionId]; // Pass the request body explicitly for GET/DELETE requests as well await transport.handleRequest(req, res, req.body || null); })().catch((error) => { console.error("Error handling session 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 SSE app.get("/mcp", handleSessionRequest); // Handle DELETE requests for session termination app.delete("/mcp", handleSessionRequest); // Health check endpoint app.get("/health", (req, res) => { res.json({ status: "ok", activeSessions: Object.keys(transports).length, timestamp: new Date().toISOString(), }); }); const PORT = process.env.BE_USER_CHAT_MCP_PORT || 30001; function main() { try { app.listen(PORT, () => { console.error(`be-user-chat-mcp HTTP server running on port ${PORT}`); console.error(`MCP endpoint: http://localhost:${PORT}/mcp`); console.error(`Health check: http://localhost:${PORT}/health`); }); } catch (error) { console.error("Failed to start HTTP server:", error); process.exit(1); } } main(); //# sourceMappingURL=server.js.map