hyperframes-mcp-cloud / server.mjs
Alshokairy's picture
Add whisper.cpp support and retest snapshot
81485b9 verified
Raw History Blame Contribute Delete
27.5 kB
#!/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/<project>/<path>
*/
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/<project>/<path>" },
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}`);
});