235 lines
9.9 KiB
Plaintext
235 lines
9.9 KiB
Plaintext
import http from "node:http";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import os from "os";
|
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
import { MCP_PORT } from "./config/index.js";
|
|
import {
|
|
sessionCredentials,
|
|
sessionApiClients,
|
|
clearSessionCredentials
|
|
} from "./auth/index.js";
|
|
|
|
// Active SSE sessions
|
|
const activeSessions = new Map();
|
|
|
|
/**
|
|
* Create and start the MCP HTTP server for SSE transport
|
|
*/
|
|
export function startHttpServer(server) {
|
|
const mcpHttpServer = http.createServer(async (req, res) => {
|
|
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
|
|
// Handle SSE connection (GET /sse)
|
|
if (req.method === "GET" && url.pathname === "/sse") {
|
|
console.error(`[MCP HTTP] GET /sse - New SSE connection from ${req.headers.origin || req.socket.remoteAddress}`);
|
|
|
|
// Extract credentials from headers
|
|
const token = req.headers['x-acai-token'];
|
|
const tokenHash = req.headers['x-acai-token-hash'];
|
|
const website = req.headers['x-acai-website'];
|
|
|
|
// Create SSE transport
|
|
const transport = new SSEServerTransport("/message", res);
|
|
const sessionId = transport.sessionId;
|
|
|
|
// Wrap the send method to add logging and fix SSE format
|
|
const originalSend = transport.send.bind(transport);
|
|
transport.send = async (message) => {
|
|
try {
|
|
if (!transport._sseResponse) {
|
|
throw new Error('Not connected');
|
|
}
|
|
|
|
// Serialize message and ensure it's valid for SSE format
|
|
const messageJson = JSON.stringify(message);
|
|
console.error(`[MCP HTTP] SSE.send - Session ${sessionId} - Sending message (${messageJson.substring(0, 100)}...)`);
|
|
|
|
// SSE format: properly handle multiline data
|
|
// If JSON contains newlines, each line must be prefixed with "data: "
|
|
const lines = messageJson.split('\n');
|
|
let sseData = 'event: message\n';
|
|
if (lines.length === 1) {
|
|
// Single line JSON
|
|
sseData += `data: ${messageJson}\n\n`;
|
|
} else {
|
|
// Multiline JSON - prefix each line
|
|
for (const line of lines) {
|
|
sseData += `data: ${line}\n`;
|
|
}
|
|
sseData += '\n';
|
|
}
|
|
|
|
transport._sseResponse.write(sseData);
|
|
console.error(`[MCP HTTP] SSE.send - Session ${sessionId} - Message sent successfully (${sseData.length} bytes)`);
|
|
} catch (error) {
|
|
console.error(`[MCP HTTP] SSE.send - Session ${sessionId} - ERROR sending message:`, error.message);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
console.error(`[MCP HTTP] GET /sse - Session ${sessionId} created`);
|
|
|
|
// Store credentials for this session
|
|
if (token && website) {
|
|
sessionCredentials.set(sessionId, {
|
|
token,
|
|
tokenHash: tokenHash || null,
|
|
website,
|
|
profileName: 'http-session'
|
|
});
|
|
console.error(`[MCP HTTP] GET /sse - Session ${sessionId} authenticated for ${website}`);
|
|
} else {
|
|
console.warn(`[MCP HTTP] GET /sse - Session ${sessionId} started without credentials`);
|
|
}
|
|
|
|
// Store session
|
|
activeSessions.set(sessionId, transport);
|
|
|
|
// Handle transport close
|
|
transport.onclose = () => {
|
|
console.error(`[MCP HTTP] GET /sse - Session ${sessionId} closed`);
|
|
activeSessions.delete(sessionId);
|
|
clearSessionCredentials(sessionId);
|
|
};
|
|
|
|
// Connect server to transport
|
|
try {
|
|
console.error(`[MCP HTTP] GET /sse - Session ${sessionId} connecting server to transport...`);
|
|
await server.connect(transport);
|
|
console.error(`[MCP HTTP] GET /sse - Session ${sessionId} server connected successfully`);
|
|
} catch (error) {
|
|
console.error(`[MCP HTTP] GET /sse - Session ${sessionId} ERROR:`, error);
|
|
activeSessions.delete(sessionId);
|
|
clearSessionCredentials(sessionId);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle POST messages (POST /message?sessionId=xxx)
|
|
if (req.method === "POST" && url.pathname === "/message") {
|
|
const sessionId = url.searchParams.get("sessionId");
|
|
|
|
if (!sessionId) {
|
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: "Missing session ID" }));
|
|
return;
|
|
}
|
|
|
|
const transport = activeSessions.get(sessionId);
|
|
if (!transport) {
|
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
return;
|
|
}
|
|
|
|
// Read request body
|
|
let body = "";
|
|
req.on("data", (chunk) => {
|
|
body += chunk.toString();
|
|
});
|
|
|
|
req.on("end", async () => {
|
|
try {
|
|
console.error(`[MCP HTTP] POST /message - Session ${sessionId} - Received request, parsing...`);
|
|
const parsedBody = JSON.parse(body);
|
|
console.error(`[MCP HTTP] POST /message - Session ${sessionId} - Parsed body: ${JSON.stringify(parsedBody).substring(0, 100)}...`);
|
|
|
|
// Track when handlePostMessage starts and ends
|
|
const beforeTime = Date.now();
|
|
await transport.handlePostMessage(req, res, parsedBody);
|
|
const afterTime = Date.now();
|
|
|
|
console.error(`[MCP HTTP] POST /message - Session ${sessionId} - handlePostMessage completed (took ${afterTime - beforeTime}ms)`);
|
|
console.error(`[MCP HTTP] POST /message - Session ${sessionId} - Response headersSent: ${res.headersSent}, writableEnded: ${res.writableEnded}`);
|
|
} catch (error) {
|
|
console.error(`[MCP HTTP] POST /message - Session ${sessionId} - ERROR:`, error.message);
|
|
console.error(`[MCP HTTP] POST /message - Session ${sessionId} - Stack:`, error.stack);
|
|
if (!res.headersSent) {
|
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: error.message }));
|
|
}
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Health check
|
|
if (req.method === "GET" && url.pathname === "/health") {
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
activeSessions: activeSessions.size,
|
|
mode: "sse"
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// Serve generated images from temp folder
|
|
if (req.method === "GET" && url.pathname.startsWith("/generated-images/")) {
|
|
const fileId = url.pathname.split("/generated-images/")[1];
|
|
if (!fileId) {
|
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: "File ID required" }));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const tempDir = process.env.GENERATED_IMAGES_TEMP_DIR || path.join(os.tmpdir(), 'generated-images');
|
|
const filename = `generated-${fileId}.jpg`;
|
|
const filePath = path.join(tempDir, filename);
|
|
|
|
// Security: ensure file is within temp directory (prevent path traversal)
|
|
const resolvedPath = path.resolve(filePath);
|
|
const resolvedTempDir = path.resolve(tempDir);
|
|
if (!resolvedPath.startsWith(resolvedTempDir)) {
|
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: "Access denied" }));
|
|
return;
|
|
}
|
|
|
|
// Check if file exists
|
|
if (!fs.existsSync(filePath)) {
|
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: "File not found" }));
|
|
return;
|
|
}
|
|
|
|
// Read and serve file
|
|
const fileBuffer = fs.readFileSync(filePath);
|
|
res.writeHead(200, {
|
|
"Content-Type": "image/jpeg",
|
|
"Content-Length": fileBuffer.length,
|
|
"Cache-Control": "public, max-age=3600"
|
|
});
|
|
res.end(fileBuffer);
|
|
return;
|
|
} catch (error) {
|
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: error.message }));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 404 for other routes
|
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
});
|
|
|
|
mcpHttpServer.on("error", (error) => {
|
|
console.error(`[MCP] HTTP server error:`, error);
|
|
});
|
|
|
|
mcpHttpServer.listen(MCP_PORT, () => {
|
|
console.error(`[MCP] SSE server listening on http://localhost:${MCP_PORT}/sse`);
|
|
console.error(`[MCP] Clients should connect to: http://localhost:${MCP_PORT}/sse`);
|
|
console.error(`[MCP] Provide credentials via headers: X-Acai-Token, X-Acai-Website, X-Acai-Token-Hash`);
|
|
});
|
|
|
|
return mcpHttpServer;
|
|
}
|
|
|
|
export { activeSessions };
|
|
|
|
|