Skip to main content
Glama
file.ts6.38 kB
import { createWriteStream } from "node:fs"; import { promises as fs } from "node:fs"; import path from "node:path"; import { randomUUID } from "node:crypto"; import { pipeline } from "node:stream/promises"; import type { AppConfig } from "../config/index.js"; export type PreparedImageSource = "local" | "downloaded" | "base64"; export interface PreparedImage { path: string; cleanup: () => Promise<void>; source: PreparedImageSource; sizeBytes: number; } const DATA_URL_PATTERN = /^data:(?<mime>[\w/+.-]+);base64,(?<data>.+)$/i; const MIME_EXTENSION_MAP: Record<string, string> = { "image/png": ".png", "image/jpeg": ".jpg", "image/jpg": ".jpg", "image/webp": ".webp", "image/gif": ".gif", "image/bmp": ".bmp", "image/tiff": ".tiff", "image/heic": ".heic", }; function looksLikeUrl(value: string): boolean { return /^https?:\/\//i.test(value); } async function ensureTempDirectory(dir: string): Promise<void> { await fs.mkdir(dir, { recursive: true }); } export function determineExtension( pathOrMime: string, fallback = ".bin" ): string { const byMime = MIME_EXTENSION_MAP[pathOrMime.toLowerCase()]; if (byMime) { return byMime; } const ext = path.extname(pathOrMime); if (ext) { return ext.toLowerCase(); } return fallback; } const EXTENSION_MIME_MAP: Record<string, string> = Object.fromEntries( Object.entries(MIME_EXTENSION_MAP).map(([mime, ext]) => [ext, mime]) ); export function guessImageMimeType( filePath: string, fallback: string = "application/octet-stream" ): string { const ext = path.extname(filePath).toLowerCase(); if (ext && EXTENSION_MIME_MAP[ext]) { return EXTENSION_MIME_MAP[ext]; } return fallback; } async function validateImage( filePath: string, sizeLimit: number, allowedExtensions: string[] ): Promise<number> { const stats = await fs.stat(filePath); if (!stats.isFile()) { throw new Error(`Provided path is not a file: ${filePath}`); } if (stats.size === 0) { throw new Error(`Image file appears to be empty: ${filePath}`); } if (stats.size > sizeLimit) { throw new Error( `Image size ${stats.size} exceeds limit of ${sizeLimit} bytes` ); } const extension = path.extname(filePath).toLowerCase(); if (extension && !allowedExtensions.includes(extension)) { throw new Error( `Unsupported image extension "${extension}". Allowed: ${allowedExtensions.join( ", " )}` ); } return stats.size; } async function writeBufferToFile( buffer: Buffer, destination: string ): Promise<void> { await fs.writeFile(destination, buffer, { mode: 0o600 }); } export async function prepareImage( input: string, config: AppConfig ): Promise<PreparedImage> { const trimmed = input.trim(); await ensureTempDirectory(config.tempDir); if (await fileExists(trimmed)) { const absolutePath = path.resolve(trimmed); const sizeBytes = await validateImage( absolutePath, config.maxImageBytes, config.allowedImageExtensions ); return { path: absolutePath, cleanup: async () => {}, source: "local", sizeBytes, }; } const dataUrlMatch = trimmed.match(DATA_URL_PATTERN); if (dataUrlMatch?.groups) { const { mime, data } = dataUrlMatch.groups as { mime: string; data: string }; const buffer = Buffer.from(data, "base64"); if (!buffer.length) { throw new Error("Provided data URL does not contain any data."); } const extension = determineExtension(mime, ".png"); const tempPath = path.join( config.tempDir, `mcp-vision-relay-${randomUUID()}${extension}` ); await writeBufferToFile(buffer, tempPath); const sizeBytes = await validateImage( tempPath, config.maxImageBytes, config.allowedImageExtensions ); return { path: tempPath, cleanup: async () => cleanupFile(tempPath), source: "base64", sizeBytes, }; } if (looksLikeUrl(trimmed)) { const url = new URL(trimmed); const response = await fetch(url); if (!response.ok) { throw new Error( `Failed to download image. HTTP ${response.status} ${response.statusText}` ); } const contentType = response.headers.get("content-type") ?? ""; const extension = determineExtension( path.extname(url.pathname) || contentType, ".png" ); const tempPath = path.join( config.tempDir, `mcp-vision-relay-${randomUUID()}${extension}` ); const stream = createWriteStream(tempPath, { mode: 0o600 }); try { if (!response.body) { throw new Error("Download response did not include a readable body."); } await pipeline(response.body, stream); } catch (error) { await cleanupFile(tempPath); throw error; } const sizeBytes = await validateImage( tempPath, config.maxImageBytes, config.allowedImageExtensions ); return { path: tempPath, cleanup: async () => cleanupFile(tempPath), source: "downloaded", sizeBytes, }; } // Raw base64 without data URL prefix if (/^[a-z0-9+/=\s]+$/i.test(trimmed)) { try { const buffer = Buffer.from(trimmed.replace(/\s+/g, ""), "base64"); if (!buffer.length) { throw new Error("Base64 data did not decode to any bytes."); } const tempPath = path.join( config.tempDir, `mcp-vision-relay-${randomUUID()}.png` ); await writeBufferToFile(buffer, tempPath); const sizeBytes = await validateImage( tempPath, config.maxImageBytes, config.allowedImageExtensions ); return { path: tempPath, cleanup: async () => cleanupFile(tempPath), source: "base64", sizeBytes, }; } catch (error) { throw new Error( `Failed to decode base64 image data: ${(error as Error).message}` ); } } throw new Error( "Unsupported image input. Provide a local file path, http(s) URL, or base64 string." ); } async function fileExists(candidate: string): Promise<boolean> { try { await fs.access(candidate); return true; } catch { return false; } } async function cleanupFile(filePath: string): Promise<void> { await fs .unlink(filePath) .catch(() => { /* ignore */ }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ah-wq/mcp-vision-relay'

If you have feedback or need assistance with the MCP directory API, please join our Discord server