UNPKG

comfyui-mcp

Version:

Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),

128 lines • 5.07 kB
import { realpath, stat } from "node:fs/promises"; import { basename, extname, isAbsolute, relative, resolve, sep } from "node:path"; import { config } from "../config.js"; import { AssetRegistry } from "./asset-registry.js"; import { getOutputImage } from "./image-management.js"; import { uploadToStorage, } from "./storage/index.js"; import { ValidationError } from "../utils/errors.js"; import { logger } from "../utils/logger.js"; import { redactUrlForLogs } from "./download-auth.js"; const MIME_BY_EXTENSION = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif", ".bmp": "image/bmp", ".tiff": "image/tiff", ".tif": "image/tiff", ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm", ".wav": "audio/wav", ".mp3": "audio/mpeg", ".flac": "audio/flac", }; function getOutputDir() { if (!config.comfyuiPath) { throw new ValidationError("COMFYUI_PATH is not configured. Set the COMFYUI_PATH environment variable."); } return resolve(config.comfyuiPath, "output"); } function isInsideOrEqual(root, candidate) { const rel = relative(root, candidate); return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)); } function assertInsideOutput(root, candidate, label) { if (!isInsideOrEqual(root, candidate) || candidate === root) { throw new ValidationError(`${label} must stay within the ComfyUI output directory.`); } } function resolveOutputPath(path) { if (path.trim().length === 0) { throw new ValidationError("path must be a non-empty path."); } const outputDir = getOutputDir(); const resolved = isAbsolute(path) ? resolve(path) : resolve(outputDir, path); if (resolved === outputDir || !resolved.startsWith(outputDir + sep)) { throw new ValidationError("path must stay within the ComfyUI output directory."); } return resolved; } function inferMimeType(filename) { return MIME_BY_EXTENSION[extname(filename).toLowerCase()] ?? "application/octet-stream"; } async function sourceFromPath(path) { const lexicalPath = resolveOutputPath(path); const root = await realpath(getOutputDir()); // unknown-ok: the caller REFUSES on undefined (the throw immediately below), so a // failed observation and a genuine absence both fail closed. The only cost is that // the message says "not found" for e.g. a permission error — noted, not dangerous, // because nothing proceeds on the unknown. const sourcePath = await realpath(lexicalPath).catch(() => undefined); if (!sourcePath) { throw new ValidationError(`Upload source not found: ${lexicalPath}`); } assertInsideOutput(root, sourcePath, "path"); // unknown-ok: undefined fails the isFile() test below and refuses, same as a // genuine non-file. A path realpath just resolved that cannot then be stat-ed is // not usable either way. const info = await stat(sourcePath).catch(() => undefined); if (!info?.isFile()) { throw new ValidationError(`Upload source not found: ${sourcePath}`); } const filename = basename(sourcePath); return { path: sourcePath, filename, contentType: inferMimeType(filename), bytes: info.size, }; } async function sourceFromAsset(assetId) { const record = AssetRegistry.get(assetId); if (!record) { throw new ValidationError(`No asset found for id "${assetId}". It may have expired or never been registered.`); } const validType = record.type === "output" || record.type === "input" || record.type === "temp"; const fetchType = validType ? record.type : "output"; const image = await getOutputImage(record.filename, fetchType, record.subfolder); const data = Buffer.from(image.base64, "base64"); return { data, filename: image.filename, contentType: image.mimeType, bytes: data.length, }; } function redactDestinationForLogs(destination) { if ("http" in destination) return { http: { url: redactUrlForLogs(destination.http.url) } }; return destination; } export async function uploadOutput(opts) { if (Boolean(opts.asset_id) === Boolean(opts.path)) { throw new ValidationError("Provide exactly one upload source: asset_id or path."); } const source = opts.asset_id ? await sourceFromAsset(opts.asset_id) : await sourceFromPath(opts.path); logger.info("Uploading ComfyUI output to cloud storage", { source: source.path ?? `asset:${opts.asset_id}`, destination: redactDestinationForLogs(opts.destination), }); const upload = await uploadToStorage(source, opts.destination); return { source: { filename: source.filename, path: source.path, asset_id: opts.asset_id, mime_type: source.contentType, bytes: source.bytes, }, uploads: [upload], }; } //# sourceMappingURL=storage-upload.js.map