Initial commit

This commit is contained in:
Jordan
2026-04-01 23:16:45 +01:00
commit 91cfdaee72
200 changed files with 25589 additions and 0 deletions

View File

@@ -0,0 +1,234 @@
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 };