#!/usr/bin/env node /** * HyperFrames MCP Cloud Server for ChatGPT Connectors / Apps SDK. * * ChatGPT-ready endpoints: * Streamable HTTP MCP: /mcp (recommended by current Apps SDK quickstart) * Legacy SSE MCP: /sse + /messages (for connector forms still showing /sse examples) * Health: /health * Files: /files// */ import { createServer } from "node:http"; import { execFile } from "node:child_process"; import { randomUUID } from "node:crypto"; import path from "node:path"; import fs from "node:fs"; import { fileURLToPath } from "node:url"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { z } from "zod"; const APP_ROOT = path.dirname(fileURLToPath(import.meta.url)); const PORT = parseInt(process.env.PORT || process.env.MCP_PORT || "7860", 10); const HOST = process.env.HOST || "0.0.0.0"; const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || "/tmp/hyperframes-work"; const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL || ""; const AUTH_TOKEN = process.env["AUTH" + "_TOKEN"] || ""; const MCP_PATH = "/mcp"; const SSE_PATH = "/sse"; const MESSAGE_PATH = "/messages"; const MAX_OUTPUT = 60_000; fs.mkdirSync(WORKSPACE_ROOT, { recursive: true }); let lastBaseUrl = PUBLIC_BASE_URL; const sseTransports = new Map(); const EXAMPLES = [ "blank", "warm-grain", "play-mode", "swiss-grid", "vignelli", "decision-tree", "kinetic-type", "product-promo", "nyt-graph", ]; const QUALITY = ["draft", "standard", "high"]; const FORMAT = ["mp4", "webm"]; const ALLOWED_GENERIC_COMMANDS = [ "add", "benchmark", "browser", "capture", "catalog", "compositions", "doctor", "info", "init", "inspect", "layout", "lint", "play", "publish", "remove-background", "render", "snapshot", "transcribe", "tts", "validate", "version", ]; function clipText(s, n = MAX_OUTPUT) { s = String(s || ""); return s.length > n ? s.slice(0, n) + `\n\n[truncated ${s.length - n} chars]` : s; } function safeProjectPath(projectName) { if (!projectName || !/^[a-zA-Z0-9._-]+$/.test(projectName)) { throw new Error("projectName must contain only letters, numbers, dot, underscore, or hyphen"); } const resolved = path.resolve(WORKSPACE_ROOT, projectName); const root = path.resolve(WORKSPACE_ROOT); if (!resolved.startsWith(root)) throw new Error("Invalid project path"); return resolved; } function safeFilePath(projectName, filePath) { if (!filePath || path.isAbsolute(filePath) || filePath.split(/[\\/]+/).includes("..")) { throw new Error("filePath must be a safe relative path"); } const projectDir = safeProjectPath(projectName); const resolved = path.resolve(projectDir, filePath); if (!resolved.startsWith(projectDir)) throw new Error("Invalid file path"); return resolved; } function safeMaybeAbsoluteProjectFile(projectName, maybePath, fallbackName) { if (!maybePath) return safeFilePath(projectName, fallbackName); if (path.isAbsolute(maybePath)) { const projectDir = safeProjectPath(projectName); const resolved = path.resolve(maybePath); if (!resolved.startsWith(projectDir)) throw new Error("Absolute output must stay inside the project folder"); return resolved; } return safeFilePath(projectName, maybePath); } function fileUrl(projectName, filePath) { const rel = `/files/${encodeURIComponent(projectName)}/${filePath.split(/[\\/]+/).map(encodeURIComponent).join("/")}`; return lastBaseUrl ? `${lastBaseUrl}${rel}` : rel; } function projectTree(projectDir, depth = 3, prefix = "") { if (!fs.existsSync(projectDir)) return "(missing)"; const lines = []; function walk(dir, level, relPrefix) { if (level > depth) return; for (const ent of fs.readdirSync(dir, { withFileTypes: true }).slice(0, 80)) { if (ent.name === "node_modules" || ent.name === ".git") continue; const rel = path.join(relPrefix, ent.name); lines.push(rel + (ent.isDirectory() ? "/" : "")); if (ent.isDirectory()) walk(path.join(dir, ent.name), level + 1, rel); } } walk(projectDir, 1, prefix); return lines.join("\n") || "(empty)"; } function runCli(args, cwd = WORKSPACE_ROOT, timeoutMs = 300000) { return new Promise((resolve) => { const env = { ...process.env, PRODUCER_FORCE_SCREENSHOT: process.env.PRODUCER_FORCE_SCREENSHOT || "true", PUPPETEER_CACHE_DIR: process.env.PUPPETEER_CACHE_DIR || "/app/.cache/puppeteer", NODE_NO_WARNINGS: "1", }; const localBin = path.join(APP_ROOT, "node_modules", ".bin", process.platform === "win32" ? "hyperframes.cmd" : "hyperframes"); const hasLocalBin = fs.existsSync(localBin); const useShell = process.platform === "win32"; const cliBin = process.env.HYPERFRAMES_BIN || (hasLocalBin ? localBin : "npx"); const cliArgs = hasLocalBin || process.env.HYPERFRAMES_BIN ? args.map(String) : ["hyperframes", ...args.map(String)]; execFile( cliBin, cliArgs, { cwd, env, timeout: timeoutMs, maxBuffer: 1024 * 1024 * 20, shell: useShell }, (error, stdout, stderr) => { resolve({ stdout: stdout || "", stderr: stderr || "", exitCode: error ? error.code || 1 : 0, error: error ? error.message : "", command: `${path.basename(cliBin)} ${cliArgs.join(" ")}`, }); }, ); }); } function resultText(label, result, extra = "") { return [ `${label}`, `Exit: ${result.exitCode}`, `Command: ${result.command}`, extra, result.stdout ? `STDOUT:\n${clipText(result.stdout)}` : "", result.stderr ? `STDERR:\n${clipText(result.stderr)}` : "", result.error && result.exitCode !== 0 ? `ERROR:\n${clipText(result.error)}` : "", ] .filter(Boolean) .join("\n\n"); } function text(content, structuredContent = {}) { return { content: [{ type: "text", text: String(content) }], structuredContent }; } function meta(name, visibility = ["model", "app"]) { return { "openai/toolInvocation/invoking": `Running ${name}`.slice(0, 64), "openai/toolInvocation/invoked": `${name} complete`.slice(0, 64), ui: { visibility }, }; } const projectNameSchema = z.string().regex(/^[a-zA-Z0-9._-]+$/).describe("Cloud workspace project name"); const filePathSchema = z.string().describe("Safe relative path inside the project, e.g. index.html or assets/data.json"); function createHyperframesServer() { const server = new McpServer({ name: "hyperframes-mcp-cloud", version: "1.1.0" }); server.registerTool( "hyperframes_doctor", { title: "HyperFrames doctor", description: "Diagnose HyperFrames cloud environment: Node, FFmpeg/FFprobe, Chrome headless, Docker, memory.", inputSchema: {}, _meta: meta("doctor"), annotations: { readOnlyHint: true, openWorldHint: false }, }, async () => { const r = await runCli(["doctor"], WORKSPACE_ROOT, 180000); return text(resultText("HyperFrames doctor", r)); }, ); server.registerTool( "hyperframes_info", { title: "HyperFrames info", description: "Show HyperFrames CLI/runtime information.", inputSchema: {}, _meta: meta("info"), annotations: { readOnlyHint: true, openWorldHint: false }, }, async () => { const r = await runCli(["info"], WORKSPACE_ROOT, 120000); return text(resultText("HyperFrames info", r)); }, ); server.registerTool( "hyperframes_list_projects", { title: "List HyperFrames projects", description: "List projects and top-level files in the cloud workspace.", inputSchema: {}, _meta: meta("list projects"), annotations: { readOnlyHint: true, openWorldHint: false }, }, async () => { const projects = fs.readdirSync(WORKSPACE_ROOT, { withFileTypes: true }).map((d) => d.name + (d.isDirectory() ? "/" : "")); return text(`Workspace: ${WORKSPACE_ROOT}\n\n${projects.join("\n") || "(empty)"}`, { projects }); }, ); server.registerTool( "hyperframes_init", { title: "Initialize project", description: "Create a new HyperFrames project scaffold in the cloud workspace.", inputSchema: { projectName: projectNameSchema, example: z.enum(EXAMPLES).default("blank").describe("Starter template"), }, _meta: meta("init"), annotations: { destructiveHint: false, openWorldHint: false }, }, async ({ projectName, example = "blank" }) => { const projectDir = safeProjectPath(projectName); const r = await runCli(["init", projectName, "--example", example, "--non-interactive"], WORKSPACE_ROOT, 240000); return text(resultText("Project initialized", r, `Project: ${projectDir}\nTree:\n${projectTree(projectDir)}`), { projectName, projectDir }); }, ); server.registerTool( "hyperframes_write_file", { title: "Write project file", description: "Write/overwrite a file inside a HyperFrames project, e.g. index.html, DESIGN.md, SCRIPT.md, assets/data.json.", inputSchema: { projectName: projectNameSchema, filePath: filePathSchema, content: z.string() }, _meta: meta("write file"), annotations: { destructiveHint: true, openWorldHint: false }, }, async ({ projectName, filePath, content }) => { const file = safeFilePath(projectName, filePath); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, content, "utf8"); return text(`Wrote ${filePath} (${Buffer.byteLength(content, "utf8")} bytes)`, { projectName, filePath, bytes: Buffer.byteLength(content, "utf8") }); }, ); server.registerTool( "hyperframes_read_file", { title: "Read project file", description: "Read a text file inside a HyperFrames project.", inputSchema: { projectName: projectNameSchema, filePath: filePathSchema, maxChars: z.number().int().min(100).max(100000).default(60000) }, _meta: meta("read file"), annotations: { readOnlyHint: true, openWorldHint: false }, }, async ({ projectName, filePath, maxChars = 60000 }) => { const file = safeFilePath(projectName, filePath); const data = fs.readFileSync(file, "utf8"); return text(clipText(data, maxChars), { projectName, filePath, bytes: Buffer.byteLength(data, "utf8") }); }, ); server.registerTool( "hyperframes_project_tree", { title: "Project tree", description: "Show the file tree for a HyperFrames project.", inputSchema: { projectName: projectNameSchema, depth: z.number().int().min(1).max(8).default(4) }, _meta: meta("project tree"), annotations: { readOnlyHint: true, openWorldHint: false }, }, async ({ projectName, depth = 4 }) => { const projectDir = safeProjectPath(projectName); return text(projectTree(projectDir, depth), { projectName }); }, ); for (const [toolName, command, description, timeout] of [ ["hyperframes_lint", "lint", "Static structure check for HyperFrames HTML composition files.", 120000], ["hyperframes_validate", "validate", "Runtime validation: JS errors, missing assets, contrast audit.", 240000], ["hyperframes_inspect", "inspect", "Visual layout audit: overflow, off-frame elements, occluded text.", 240000], ]) { server.registerTool( toolName, { title: `HyperFrames ${command}`, description, inputSchema: { projectName: projectNameSchema, strict: z.boolean().default(false) }, _meta: meta(command), annotations: { readOnlyHint: true, openWorldHint: false }, }, async ({ projectName, strict = false }) => { const projectDir = safeProjectPath(projectName); const args = [command]; if (strict) args.push("--strict"); const r = await runCli(args, projectDir, timeout); return text(resultText(`HyperFrames ${command}`, r), { projectName, exitCode: r.exitCode }); }, ); } server.registerTool( "hyperframes_render", { title: "Render video", description: "Render a HyperFrames project to MP4/WebM. Returns a cloud download URL under /files.", inputSchema: { projectName: projectNameSchema, output: z.string().default("output.mp4").describe("Relative output path inside project"), quality: z.enum(QUALITY).default("draft"), fps: z.union([z.literal(24), z.literal(30), z.literal(60)]).default(24), format: z.enum(FORMAT).default("mp4"), browserTimeout: z.number().int().min(30).max(300).default(120), strict: z.boolean().default(false), }, _meta: meta("render"), annotations: { destructiveHint: false, openWorldHint: false }, }, async ({ projectName, output = "output.mp4", quality = "draft", fps = 24, format = "mp4", browserTimeout = 120, strict = false }) => { const projectDir = safeProjectPath(projectName); const outPath = safeMaybeAbsoluteProjectFile(projectName, output, "output.mp4"); const args = ["render", "--quality", quality, "--fps", fps, "--format", format, "--browser-timeout", browserTimeout, "--output", outPath]; if (strict) args.push("--strict"); const r = await runCli(args, projectDir, 900000); const relOut = path.relative(projectDir, outPath).replace(/\\/g, "/"); const url = fs.existsSync(outPath) ? fileUrl(projectName, relOut) : ""; return text(resultText("HyperFrames render", r, url ? `Download URL: ${url}` : `Output path: ${outPath}`), { projectName, output: relOut, url, exitCode: r.exitCode }); }, ); server.registerTool( "hyperframes_snapshot", { title: "Snapshot frames", description: "Capture PNG verification frames from a HyperFrames project. Uses the current CLI syntax: snapshot [--frames N | --at seconds] [DIR].", inputSchema: { projectName: projectNameSchema, at: z.string().default("1").describe("Comma-separated timestamps in seconds, e.g. 1 or 1,3,5"), frames: z.number().int().min(1).max(12).optional().describe("Evenly-spaced frame count; omitted when at is provided"), timeoutMs: z.number().int().min(5000).max(120000).default(30000), }, _meta: meta("snapshot"), annotations: { destructiveHint: false, openWorldHint: false }, }, async ({ projectName, at = "1", frames, timeoutMs = 30000 }) => { const projectDir = safeProjectPath(projectName); const before = new Set(fs.readdirSync(projectDir, { recursive: true }).map(String)); const args = ["snapshot"]; if (frames) args.push("--frames", frames); else args.push("--at", at.replace(/s/g, "")); args.push("--timeout", timeoutMs, projectDir); const r = await runCli(args, WORKSPACE_ROOT, Math.max(300000, timeoutMs + 60000)); const after = fs.readdirSync(projectDir, { recursive: true }).map(String); const newPngs = after.filter((p) => p.toLowerCase().endsWith(".png") && !before.has(p)).slice(0, 20); const urls = newPngs.map((p) => fileUrl(projectName, p.replace(/\\/g, "/"))); return text(resultText("HyperFrames snapshot", r, urls.length ? `Snapshot URLs:\n${urls.join("\n")}` : "No new PNG files detected."), { projectName, snapshots: urls, exitCode: r.exitCode }); }, ); server.registerTool( "hyperframes_tts", { title: "Text to speech", description: "Generate narration audio with HyperFrames TTS voices.", inputSchema: { projectName: projectNameSchema, text: z.string().min(1), voice: z.string().default("af_nova"), output: z.string().default("narration.wav"), }, _meta: meta("tts"), annotations: { destructiveHint: false, openWorldHint: false }, }, async ({ projectName, text: speechText, voice = "af_nova", output = "narration.wav" }) => { const projectDir = safeProjectPath(projectName); fs.mkdirSync(projectDir, { recursive: true }); const outPath = safeMaybeAbsoluteProjectFile(projectName, output, "narration.wav"); const r = await runCli(["tts", speechText, "--voice", voice, "--output", outPath], projectDir, 300000); const relOut = path.relative(projectDir, outPath).replace(/\\/g, "/"); const url = fs.existsSync(outPath) ? fileUrl(projectName, relOut) : ""; return text(resultText("HyperFrames TTS", r, url ? `Download URL: ${url}` : `Output path: ${outPath}`), { projectName, output: relOut, url, exitCode: r.exitCode }); }, ); server.registerTool( "hyperframes_transcribe", { title: "Transcribe audio", description: "Transcribe an audio file inside a project using HyperFrames/Whisper transcription. Defaults to tiny.en for fast cloud execution; larger models can be requested.", inputSchema: { projectName: projectNameSchema, audioPath: filePathSchema, model: z.enum(["tiny.en", "base.en", "small.en", "medium.en", "large-v3"]).default("tiny.en"), language: z.string().optional().describe("Optional language code such as en, es, ja; filters non-target speech"), json: z.boolean().default(false).describe("Ask HyperFrames to output JSON transcript details"), }, _meta: meta("transcribe"), annotations: { readOnlyHint: true, openWorldHint: false }, }, async ({ projectName, audioPath, model = "tiny.en", language, json = false }) => { const projectDir = safeProjectPath(projectName); const file = safeFilePath(projectName, audioPath); const args = ["transcribe", file, "--model", model]; if (language) args.push("--language", language); if (json) args.push("--json"); const r = await runCli(args, projectDir, 600000); return text(resultText("HyperFrames transcribe", r), { projectName, model, language: language || null, exitCode: r.exitCode }); }, ); server.registerTool( "hyperframes_capture_url", { title: "Capture website", description: "Use HyperFrames capture to convert a public URL into a project scaffold when the site permits headless access.", inputSchema: { projectName: projectNameSchema, url: z.string().url() }, _meta: meta("capture"), annotations: { openWorldHint: true, destructiveHint: false }, }, async ({ projectName, url }) => { const projectDir = safeProjectPath(projectName); const r = await runCli(["capture", url, "--output", projectDir], WORKSPACE_ROOT, 600000); return text(resultText("HyperFrames capture", r, `Project: ${projectDir}\nTree:\n${projectTree(projectDir)}`), { projectName, exitCode: r.exitCode }); }, ); server.registerTool( "hyperframes_add", { title: "Add HyperFrames block/component", description: "Run `hyperframes add` for transitions, blocks, components, or registry assets inside a project.", inputSchema: { projectName: projectNameSchema, name: z.string().min(1).describe("Block/component/transition name or --list"), extraArgs: z.array(z.string()).default([]) }, _meta: meta("add"), annotations: { destructiveHint: false, openWorldHint: true }, }, async ({ projectName, name, extraArgs = [] }) => { const projectDir = safeProjectPath(projectName); fs.mkdirSync(projectDir, { recursive: true }); const r = await runCli(["add", name, ...extraArgs], projectDir, 300000); return text(resultText("HyperFrames add", r, `Tree:\n${projectTree(projectDir)}`), { projectName, exitCode: r.exitCode }); }, ); server.registerTool( "hyperframes_remove_background", { title: "Remove image background", description: "Run HyperFrames remove-background on an image file inside a project.", inputSchema: { projectName: projectNameSchema, input: filePathSchema, output: z.string().default("background-removed.png") }, _meta: meta("remove background"), annotations: { destructiveHint: false, openWorldHint: false }, }, async ({ projectName, input, output = "background-removed.png" }) => { const projectDir = safeProjectPath(projectName); const inPath = safeFilePath(projectName, input); const outPath = safeMaybeAbsoluteProjectFile(projectName, output, "background-removed.png"); const r = await runCli(["remove-background", inPath, "--output", outPath], projectDir, 600000); const relOut = path.relative(projectDir, outPath).replace(/\\/g, "/"); const url = fs.existsSync(outPath) ? fileUrl(projectName, relOut) : ""; return text(resultText("HyperFrames remove-background", r, url ? `Download URL: ${url}` : `Output path: ${outPath}`), { projectName, output: relOut, url, exitCode: r.exitCode }); }, ); server.registerTool( "hyperframes_catalog", { title: "Catalog/list registry", description: "List HyperFrames catalog/registry items such as blocks, examples, components, and transitions.", inputSchema: { extraArgs: z.array(z.string()).default([]) }, _meta: meta("catalog"), annotations: { readOnlyHint: true, openWorldHint: true }, }, async ({ extraArgs = [] }) => { const r = await runCli(["catalog", ...extraArgs], WORKSPACE_ROOT, 180000); return text(resultText("HyperFrames catalog", r), { exitCode: r.exitCode }); }, ); server.registerTool( "hyperframes_cli", { title: "Generic HyperFrames CLI", description: "Advanced escape hatch for HyperFrames CLI features not covered by specialized tools. Command is allowlisted and args are passed directly.", inputSchema: { projectName: z.string().regex(/^[a-zA-Z0-9._-]+$/).optional().describe("Optional project cwd"), command: z.enum(ALLOWED_GENERIC_COMMANDS), args: z.array(z.string()).default([]), timeoutSeconds: z.number().int().min(5).max(900).default(300), }, _meta: meta("cli"), annotations: { openWorldHint: true, destructiveHint: true }, }, async ({ projectName, command, args = [], timeoutSeconds = 300 }) => { const cwd = projectName ? safeProjectPath(projectName) : WORKSPACE_ROOT; fs.mkdirSync(cwd, { recursive: true }); const r = await runCli([command, ...args], cwd, timeoutSeconds * 1000); return text(resultText(`HyperFrames ${command}`, r), { projectName: projectName || null, command, exitCode: r.exitCode }); }, ); return server; } function authorized(req) { if (!AUTH_TOKEN) return true; const h = req.headers.authorization || ""; return h === `Bearer ${AUTH_TOKEN}`; } function setCors(req, res) { const origin = req.headers.origin || "*"; res.setHeader("Access-Control-Allow-Origin", origin === "null" ? "*" : origin); res.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "content-type, mcp-session-id, authorization"); res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id"); } function updateBaseUrl(req) { if (PUBLIC_BASE_URL) { lastBaseUrl = PUBLIC_BASE_URL.replace(/\/$/, ""); return; } const proto = req.headers["x-forwarded-proto"] || "https"; const host = req.headers["x-forwarded-host"] || req.headers.host; if (host) lastBaseUrl = `${proto}://${host}`.replace(/\/$/, ""); } function sendJson(res, code, obj) { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); } function serveFile(req, res, url) { const parts = url.pathname.split("/").slice(2).map(decodeURIComponent); const projectName = parts.shift(); const filePath = parts.join("/"); const file = safeFilePath(projectName, filePath); if (!fs.existsSync(file) || !fs.statSync(file).isFile()) { res.writeHead(404).end("Not found"); return; } const ext = path.extname(file).toLowerCase(); const type = ext === ".mp4" ? "video/mp4" : ext === ".webm" ? "video/webm" : ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : ext === ".wav" ? "audio/wav" : ext === ".mp3" ? "audio/mpeg" : "application/octet-stream"; res.writeHead(200, { "content-type": type, "cache-control": "public, max-age=3600" }); fs.createReadStream(file).pipe(res); } const httpServer = createServer(async (req, res) => { try { if (!req.url) return res.writeHead(400).end("Missing URL"); const url = new URL(req.url, `http://${req.headers.host || "localhost"}`); updateBaseUrl(req); setCors(req, res); if (req.method === "OPTIONS") return res.writeHead(204).end(); if (!authorized(req)) return res.writeHead(401).end("Unauthorized"); if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/health")) { return sendJson(res, 200, { status: "ok", name: "hyperframes-mcp-cloud", version: "1.1.0", endpoints: { mcp: "/mcp", sse: "/sse", health: "/health", files: "/files//" }, workspace: WORKSPACE_ROOT, sessions: sseTransports.size, uptime: process.uptime(), }); } if (req.method === "GET" && url.pathname.startsWith("/files/")) { return serveFile(req, res, url); } // ChatGPT Apps SDK recommended Streamable HTTP MCP endpoint. const MCP_METHODS = new Set(["POST", "GET", "DELETE"]); if (url.pathname === MCP_PATH && req.method && MCP_METHODS.has(req.method)) { const server = createHyperframesServer(); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true }); res.on("close", () => { transport.close().catch(() => {}); server.close().catch(() => {}); }); await server.connect(transport); await transport.handleRequest(req, res); return; } // Legacy SSE endpoint for connector UIs that still show https://.../sse examples. if (url.pathname === SSE_PATH && req.method === "GET") { const transport = new SSEServerTransport(MESSAGE_PATH, res); const server = createHyperframesServer(); sseTransports.set(transport.sessionId, { transport, server }); res.on("close", () => { sseTransports.delete(transport.sessionId); transport.close().catch(() => {}); server.close().catch(() => {}); }); await server.connect(transport); return; } if (url.pathname === MESSAGE_PATH && req.method === "POST") { const sessionId = url.searchParams.get("sessionId"); const entry = sseTransports.get(sessionId); if (!entry) return res.writeHead(404).end("Session not found"); await entry.transport.handlePostMessage(req, res); return; } // OAuth/discovery paths not in use. Return clean 404 so ChatGPT wizard does not show 502. if (url.pathname.includes("oauth") || url.pathname.includes(".well-known")) { return res.writeHead(404, { "content-type": "text/plain" }).end("Not configured"); } return res.writeHead(404).end("Not found"); } catch (err) { console.error("Request error", err); if (!res.headersSent) res.writeHead(500).end("Internal server error"); } }); httpServer.listen(PORT, HOST, () => { console.log(`HyperFrames MCP Cloud listening on http://${HOST}:${PORT}`); console.log(`Streamable HTTP: ${MCP_PATH}`); console.log(`SSE compatibility: ${SSE_PATH}`); });