feat: add HTTP/SSE transport for remote MCP via Cloudflare Tunnel

- TRANSPORT=http starts HTTP server on PORT (default 3100)
- SSE endpoint at /sse, message endpoint at /messages
- Health check at /health
- TRANSPORT=stdio (default) unchanged for local Claude Code usage
- Fix: unused _format variable in matcher.ts
This commit is contained in:
Rene Fichtmueller 2026-03-28 18:41:32 +08:00
parent 65edff142b
commit d3250b95bb
3 changed files with 4276 additions and 5 deletions

4209
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,10 @@
* @see https://modelcontextprotocol.io * @see https://modelcontextprotocol.io
*/ */
import * as http from "http";
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { import {
CallToolRequestSchema, CallToolRequestSchema,
@ -234,13 +237,72 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Start server // Start server — stdio (local) or http (remote / Cloudflare Tunnel)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function main(): Promise<void> { async function startHttp(): Promise<void> {
const transport = new StdioServerTransport(); const port = parseInt(process.env["PORT"] ?? "3100", 10);
// Track active SSE transports by session id
const sessions: Record<string, SSEServerTransport> = {};
const httpServer = http.createServer(async (req, res) => {
try {
if (req.url === "/health" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", service: "PaperCortex", transport: "http" }));
return;
}
// SSE endpoint — client opens a long-lived connection
if (req.url === "/sse" && req.method === "GET") {
const transport = new SSEServerTransport("/messages", res);
sessions[transport.sessionId] = transport;
res.on("close", () => {
delete sessions[transport.sessionId];
});
await server.connect(transport); await server.connect(transport);
return;
}
// Message endpoint — client posts tool calls here
if (req.url?.startsWith("/messages") && req.method === "POST") {
const sessionId = new URL(req.url, `http://localhost`).searchParams.get("sessionId");
const transport = sessionId ? sessions[sessionId] : undefined;
if (transport) {
await transport.handlePostMessage(req, res);
} else {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Session not found" }));
}
return;
}
res.writeHead(404);
res.end("Not found");
} catch (err) {
console.error("HTTP handler error:", err);
if (!res.headersSent) {
res.writeHead(500);
res.end("Internal error");
}
}
});
httpServer.listen(port, () => {
console.error(`PaperCortex MCP Server running on HTTP port ${port} (SSE at /sse)`);
});
}
async function main(): Promise<void> {
const transport = (process.env["TRANSPORT"] ?? "stdio").toLowerCase();
if (transport === "http") {
await startHttp();
} else {
const stdioTransport = new StdioServerTransport();
await server.connect(stdioTransport);
console.error("PaperCortex MCP Server running on stdio"); console.error("PaperCortex MCP Server running on stdio");
}
} }
main().catch((error) => { main().catch((error) => {

View File

@ -96,7 +96,7 @@ export function createTransactionMatcher(): TransactionMatcher {
// TODO: Add support for different CSV delimiters (semicolon for German exports) // TODO: Add support for different CSV delimiters (semicolon for German exports)
// TODO: Handle different date formats (DD.MM.YYYY, YYYY-MM-DD, MM/DD/YYYY) // TODO: Handle different date formats (DD.MM.YYYY, YYYY-MM-DD, MM/DD/YYYY)
const _format = format; // Acknowledge format parameter for future use void format; // reserved for future format auto-detection
const records = parse(raw, { const records = parse(raw, {
columns: true, columns: true,