@channel.io/be-user-chat-mcp
Version:
Backend UserChat Model Context Protocol server for Channel.io
200 lines • 7.72 kB
JavaScript
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