Spaces:
Runtime error
Runtime error
Download server.mjs from Alshokairy/hyperframes-mcp-cloud: direct link, hf CLI and curl.
- Browser
- Download file 27.5 kB
-
https://huggingface.co/spaces/Alshokairy/hyperframes-mcp-cloud/resolve/main/server.mjs
- Command line
-
hf download hf://spaces/Alshokairy/hyperframes-mcp-cloud/server.mjs
-
curl -L -o server.mjs https://huggingface.co/spaces/Alshokairy/hyperframes-mcp-cloud/resolve/main/server.mjs
27.5 kB
| /** | |
| * 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}`); | |
| }); | |