UNPKG

aihubmix-image-mcp

Version:

MCP server for AIHUBMIX image generation with URL support (NANO BANANA)

872 lines (862 loc) 41.4 kB
#!/usr/bin/env node "use strict"; /** * AIHUBMIX Image MCP Server * Simple & Fast - Do one thing and do it well */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const axios_1 = __importDefault(require("axios")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); let supabase = null; try { // Lazy create client only when env exists const SUPABASE_URL = process.env.SUPABASE_URL; const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY; if (SUPABASE_URL && SUPABASE_ANON_KEY) { // dynamic import to avoid hard dep at runtime if not used // eslint-disable-next-line @typescript-eslint/no-var-requires const { createClient } = require("@supabase/supabase-js"); supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); } } catch (_e) { // ignore: supabase not available } // Load config - support command line arguments and config file const configPath = path.resolve(__dirname, '../config.json'); let config = { apiKey: process.env.AIHUBMIX_API_KEY || '', baseUrl: 'https://aihubmix.com/v1', model: 'gemini-2.5-flash-image-preview', timeout: 120000 }; // Load from config file if (fs.existsSync(configPath)) { config = { ...config, ...JSON.parse(fs.readFileSync(configPath, 'utf8')) }; } // Support command line API key: aihubmix-image-mcp --api-key sk-xxx const args = process.argv; const apiKeyIndex = args.indexOf('--api-key'); if (apiKeyIndex !== -1 && args[apiKeyIndex + 1]) { config.apiKey = args[apiKeyIndex + 1]; } // Helper function to fetch image from URL or local file and convert to base64 async function fetchImageAsBase64(imagePath) { try { console.error(`🔗 Processing image: ${imagePath}`); // Check if it's a local file path if (imagePath.startsWith('file://') || imagePath.startsWith('/') || imagePath.includes(':\\')) { // Handle local file let filePath = imagePath; if (filePath.startsWith('file://')) { filePath = filePath.replace('file://', ''); } console.error(`📁 Reading local file: ${filePath}`); if (!fs.existsSync(filePath)) { throw new Error(`Local file not found: ${filePath}`); } const imageBuffer = fs.readFileSync(filePath); const base64Data = imageBuffer.toString('base64'); // Determine MIME type from file extension let mimeType = 'image/png'; const lowerPath = filePath.toLowerCase(); if (lowerPath.endsWith('.jpg') || lowerPath.endsWith('.jpeg')) { mimeType = 'image/jpeg'; } else if (lowerPath.endsWith('.png')) { mimeType = 'image/png'; } else if (lowerPath.endsWith('.gif')) { mimeType = 'image/gif'; } else if (lowerPath.endsWith('.webp')) { mimeType = 'image/webp'; } console.error(`✅ Local file processed: ${base64Data.length} chars, type: ${mimeType}`); return { data: base64Data, mimeType }; } else { // Handle HTTP/HTTPS URL const response = await axios_1.default.get(imagePath, { responseType: 'arraybuffer', timeout: 30000, headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } }); // Convert to base64 const base64Data = Buffer.from(response.data).toString('base64'); // Determine MIME type from response headers or URL extension let mimeType = response.headers['content-type'] || 'image/png'; if (!mimeType.startsWith('image/')) { // Fallback: detect from URL extension const lowerUrl = imagePath.toLowerCase(); if (lowerUrl.includes('.jpg') || lowerUrl.includes('.jpeg')) { mimeType = 'image/jpeg'; } else if (lowerUrl.includes('.png')) { mimeType = 'image/png'; } else if (lowerUrl.includes('.gif')) { mimeType = 'image/gif'; } else if (lowerUrl.includes('.webp')) { mimeType = 'image/webp'; } else { mimeType = 'image/png'; // default } } console.error(`✅ URL image fetched: ${base64Data.length} chars, type: ${mimeType}`); return { data: base64Data, mimeType }; } } catch (error) { console.error(`❌ Failed to process image ${imagePath}: ${error.message}`); throw new Error(`Failed to process image (${imagePath}): ${error.message}`); } } // Image generation function with multi-image support (now supports URLs!) async function generateImage(prompt, inputImages) { // Validate API key if (!config.apiKey || config.apiKey === 'your-aihubmix-api-key-here' || config.apiKey === 'sk-your-aihubmix-api-key-here') { throw new Error('API key not configured. Set AIHUBMIX_API_KEY or edit config.json'); } // Validate prompt if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) { throw new Error('Prompt is required and must be a non-empty string'); } // Validate input images if provided if (inputImages && inputImages.length > 0) { if (!Array.isArray(inputImages)) { throw new Error('Images must be an array'); } if (inputImages.length > 3600) { throw new Error(`Too many images provided. Maximum allowed: 3600, provided: ${inputImages.length}`); } for (let i = 0; i < inputImages.length; i++) { const img = inputImages[i]; if (!img || typeof img !== 'object') { throw new Error(`Image ${i + 1} must be an object`); } if (img.type !== 'image') { throw new Error(`Image ${i + 1} type must be 'image', got: ${img.type}`); } if (!img.data || typeof img.data !== 'string' || img.data.trim() === '') { throw new Error(`Image ${i + 1} data must be a non-empty string`); } // Check if it's a URL, local file path, data URL, or base64 data const isUrl = img.data.startsWith('http://') || img.data.startsWith('https://'); const isLocalFile = img.data.startsWith('file://') || img.data.startsWith('/') || img.data.includes(':\\'); const isDataUrl = img.data.startsWith('data:image/'); if (isUrl) { // URL validation - basic URL format check try { new URL(img.data); } catch (error) { throw new Error(`Image ${i + 1} contains invalid URL: ${img.data}`); } } else if (isLocalFile) { // Local file path validation let filePath = img.data; if (filePath.startsWith('file://')) { filePath = filePath.replace('file://', ''); } if (!fs.existsSync(filePath)) { throw new Error(`Image ${i + 1} local file not found: ${filePath}`); } } else if (isDataUrl) { // Data URL validation const base64Data = img.data.split(',')[1]; if (!base64Data || base64Data.length === 0) { throw new Error(`Image ${i + 1} has invalid data URL format`); } if (!/^[A-Za-z0-9+/=]+$/.test(base64Data)) { throw new Error(`Image ${i + 1} contains invalid base64 characters in data URL`); } } else { // Raw base64 validation if (!/^[A-Za-z0-9+/=]+$/.test(img.data)) { throw new Error(`Image ${i + 1} contains invalid base64 characters`); } } } } // Build content array starting with text prompt const content = [{ type: "text", text: prompt.trim() }]; // Add input images if provided (now supports URLs!) if (inputImages && inputImages.length > 0) { for (const img of inputImages) { let imageData; let mimeType; // Check if it's a URL or local file that needs to be processed if (img.data.startsWith('http://') || img.data.startsWith('https://') || img.data.startsWith('file://') || img.data.startsWith('/') || img.data.includes(':\\')) { // Fetch image from URL or local file and convert to base64 const fetchResult = await fetchImageAsBase64(img.data); imageData = fetchResult.data; mimeType = fetchResult.mimeType; } else if (img.data.startsWith('data:image/')) { // Extract base64 data from data URL imageData = img.data.split(',')[1]; mimeType = img.mimeType || "image/png"; } else { // Assume raw base64 data imageData = img.data; mimeType = img.mimeType || "image/png"; } content.push({ type: "image", inline_data: { mime_type: mimeType, data: imageData } }); } } const response = await axios_1.default.post(`${config.baseUrl}/chat/completions`, { model: config.model, messages: [ { role: "user", content: content } ], modalities: ["text", "image"], temperature: 0.7 }, { headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' }, timeout: config.timeout }); // Extract image data const choice = response.data?.choices?.[0]; const tryExtract = (arr) => { if (!Array.isArray(arr)) return null; for (const item of arr) { // snake_case if (item?.inline_data?.data) return item.inline_data.data; // camelCase if (item?.inlineData?.data) return item.inlineData.data; // direct string data on item if (typeof item?.data === 'string') return item.data; // aliases if (item?.image_base64) return item.image_base64; if (item?.b64_json) return item.b64_json; } return null; }; // 1) MCP chat style let b64 = tryExtract(choice?.message?.multi_mod_content) || tryExtract(choice?.message?.content); // 2) Google-style candidates[].content.parts[].inlineData if (!b64) { const candidates = response.data?.candidates; const parts = Array.isArray(candidates) ? candidates[0]?.content?.parts : null; b64 = tryExtract(parts); } // 3) OpenAI images style: data[].b64_json if (!b64) { const dataArr = response.data?.data; if (Array.isArray(dataArr) && dataArr.length > 0) { b64 = dataArr[0]?.b64_json || (typeof dataArr[0]?.data === 'string' ? dataArr[0].data : null); } } if (b64) return b64; // 4) Optional debug diagnostics if (process.env.AIHUBMIX_DEBUG === '1') { const topKeys = Object.keys(response.data || {}).slice(0, 10); const msgKeys = Object.keys(choice?.message || {}).slice(0, 10); throw new Error(`No image data found in response (diag: topKeys=${topKeys.join('|')}; messageKeys=${msgKeys.join('|')})`); } throw new Error('No image data found in response'); } async function generateVideo(prompt, inputMedia) { // Validate API key if (!config.apiKey || config.apiKey === 'your-aihubmix-api-key-here' || config.apiKey === 'sk-your-aihubmix-api-key-here') { throw new Error('API key not configured. Set AIHUBMIX_API_KEY or edit config.json'); } if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) { throw new Error('Prompt is required and must be a non-empty string'); } // Build content array starting with text prompt const content = [{ type: "text", text: prompt.trim() }]; // Optional inputs (images/videos) & inline_data (<= 20MB) if (inputMedia && inputMedia.length > 0) { if (!Array.isArray(inputMedia)) throw new Error('inputs must be an array'); for (let i = 0; i < inputMedia.length; i++) { const m = inputMedia[i]; if (!m || typeof m !== 'object') throw new Error(`inputs[${i}] must be an object`); const kind = m.type; if (kind !== 'image' && kind !== 'video') throw new Error(`inputs[${i}].type must be 'image' or 'video'`); if (!m.data || typeof m.data !== 'string' || m.data.trim() === '') { throw new Error(`inputs[${i}].data must be a non-empty string`); } let dataB64, mimeType = m.mimeType; const looksUrlOrPath = m.data.startsWith('http://') || m.data.startsWith('https://') || m.data.startsWith('file://') || m.data.startsWith('/') || m.data.includes(':\\'); if (m.data.startsWith('data:')) { // data URL const parts = m.data.split(','); dataB64 = parts[1] || ''; if (!mimeType) { const mt = (parts[0] || '').split(';')[0].replace(/^data:/, ''); mimeType = mt || (kind === 'video' ? 'video/mp4' : 'image/png'); } } else if (looksUrlOrPath) { const r = await fetchImageAsBase64(m.data); dataB64 = r.data; mimeType = r.mimeType || (kind === 'video' ? 'video/mp4' : 'image/png'); } else { // assume raw base64 dataB64 = m.data; mimeType = mimeType || (kind === 'video' ? 'video/mp4' : 'image/png'); } // For now, pass as image/video inline_data (OpenAI兼容形态) if (kind === 'image') { content.push({ type: "image_url", image_url: { url: `data:${mimeType};base64,${dataB64}` } }); } else { // 视频输入(小文件)—极少用,主要场景是两帧图转视频 content.push({ type: "input_video", inline_data: { mime_type: mimeType, data: dataB64 } }); } } } const response = await axios_1.default.post(`${config.baseUrl}/chat/completions`, { model: 'veo-3', messages: [{ role: "user", content }], temperature: 0.2 }, { headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' }, timeout: Math.max(config.timeout, 180000) // 视频耗时更长 }); const choice = response.data?.choices?.[0]; const msg = choice?.message; let textOut = ''; const c = msg?.content; if (typeof c === 'string') { textOut = c; } else if (Array.isArray(c)) { textOut = c.map(p => { if (typeof p === 'string') return p; if (p?.text) return p.text; if (typeof p?.content === 'string') return p.content; if (p?.image_url?.url) return p.image_url.url; if (p?.url) return p.url; return ''; }).filter(Boolean).join('\n'); } if (!textOut) { textOut = JSON.stringify(msg || response.data).slice(0, 2000); } // find candidate URLs (mp4/async page) const urls = []; const re = /(https?:\/\/[^\s\]\)\}"'>]+)/g; let m; while ((m = re.exec(textOut)) !== null) { const u = m[1].replace(/[),.\]]+$/, ""); if (u.includes('.mp4') || u.includes('asyncdata.net') || u.includes('filesystem.site')) { urls.push(u); } } // Fallback: if upload error and no direct mp4/async urls, retry via /v1/responses with inline input_image if (urls.length === 0 && /uploadUserImage|INVALID_ARGUMENT|PUBLIC_ERROR_MINOR_UPLOAD/i.test(textOut || '')) { try { const parts = [{ type: 'input_text', text: prompt.trim() }]; if (Array.isArray(inputMedia) && inputMedia.length > 0) { for (let i = 0; i < inputMedia.length; i++) { const m0 = inputMedia[i]; if (!m0 || typeof m0 !== 'object') continue; if (m0.type !== 'image') continue; let dataB64_2; let mimeType_2 = m0.mimeType; const looksUrlOrPath_2 = m0.data.startsWith('http://') || m0.data.startsWith('https://') || m0.data.startsWith('file://') || m0.data.startsWith('/') || m0.data.includes(':\\'); if (m0.data.startsWith('data:')) { const ps = m0.data.split(','); dataB64_2 = ps[1] || ''; if (!mimeType_2) { const mt2 = (ps[0] || '').split(';')[0].replace(/^data:/, ''); mimeType_2 = mt2 || 'image/png'; } } else if (looksUrlOrPath_2) { const r2 = await fetchImageAsBase64(m0.data); dataB64_2 = r2.data; mimeType_2 = r2.mimeType || 'image/png'; } else { dataB64_2 = m0.data; mimeType_2 = mimeType_2 || 'image/png'; } parts.push({ type: 'input_image', image: { data: dataB64_2, mime_type: mimeType_2 } }); } } const resp2 = await axios_1.default.post(`${config.baseUrl}/responses`, { model: 'veo-3', input: [{ role: 'user', content: parts }], max_output_tokens: 128, temperature: 0.2 }, { headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' }, timeout: Math.max(config.timeout, 180000) }); let text2 = ''; if (typeof (resp2.data && resp2.data.output_text) === 'string') text2 = resp2.data.output_text; if (!text2) text2 = JSON.stringify(resp2.data).slice(0, 2000); const urls2 = []; let m2; const re2 = /(https?:\/\/[^\s\]\)\}\"'>]+)/g; while ((m2 = re2.exec(text2)) !== null) { const u2 = m2[1].replace(/[),.\]]+$/, ''); if (u2.includes('.mp4') || u2.includes('asyncdata.net') || u2.includes('filesystem.site')) { urls2.push(u2); } } return { text: `${textOut}\n\n[Fallback responses]\n${text2}`, urls: urls2 }; } catch (e) { // ignore fallback errors } } return { text: textOut, urls }; } // Create MCP server const server = new index_js_1.Server({ name: 'aihubmix-image-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, }); // List available tools server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: [ { name: 'generate_image', description: `Generate and edit images using AIHUBMIX NANO BANANA (Gemini 2.5 Flash Image). Supports text-to-image generation and multi-image operations with URL support! 🆕 NEW: Now supports image URLs and local file paths! No need to convert to base64. USAGE EXAMPLES: Basic Generation: - prompt: "A cute anime girl with blue hair, kawaii style" - prompt: "Photorealistic sunset over mountains, golden hour lighting" Local File Processing (EASIEST WAY): - prompt: "Edit this image to add a red dress" - images: [{ type: "image", data: "/path/to/your/photo.jpg" }] URL-based Image Editing: - prompt: "Edit this image to add a red dress" - images: [{ type: "image", data: "https://example.com/photo.jpg" }] Style Transfer (with URLs): - prompt: "Apply the artistic style from the first image to the second image" - images: [ { type: "image", data: "https://example.com/style.jpg" }, { type: "image", data: "https://example.com/content.jpg" } ] Scene Composition: - prompt: "Place the product from the first image into the room setting from the second image" - images: [product_url, room_url] Image Editing: - prompt: "Remove the background and make it transparent" - prompt: "Change the color of the dress to red" - images: [{ type: "image", data: "https://your-image-url.com/photo.png" }] Multi-image Fusion: - prompt: "Blend these images into a cohesive artistic composition" - images: [url1, url2, url3] ✅ Supported formats: Local file paths, Image URLs (http/https), Base64 data, Data URLs 🚀 Local file and URL support provides the best user experience in CodeBuddy!`, inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'Detailed text prompt describing the desired image or operation. For multi-image operations, describe how the input images should be used (e.g., "Apply the style from image 1 to image 2", "Place the object from image 1 into the scene from image 2"). Be specific about artistic style, lighting, composition, and any transformations needed.' }, images: { type: 'array', items: { type: 'object', properties: { type: { type: 'string', enum: ['image'], description: 'Content type indicator' }, data: { type: 'string', description: 'Image data: can be a local file path (e.g., /path/to/image.jpg), HTTP/HTTPS URL, base64 encoded data, or data URL. Local files and URLs are automatically processed and converted.' }, mimeType: { type: 'string', description: 'MIME type of the image (e.g., image/png, image/jpeg)', default: 'image/png' } }, required: ['type', 'data'] }, description: 'Optional array of input images for multi-image operations (composition, style transfer, editing). Supports up to 3600 images per request.', maxItems: 3600 }, output: { type: 'object', description: 'Output options', properties: { mode: { type: 'string', enum: ['inlineBase64', 'summary', 'file'], description: 'How to return the result image', default: 'file' }, previewChars: { type: 'number', description: 'For summary mode: number of base64 leading chars to include', default: 80 }, dir: { type: 'string', description: 'For file mode: directory to save image into (will be created if missing)', default: './generated-images' }, filename: { type: 'string', description: 'For file mode: filename to use (if omitted, auto-generated with timestamp)' } } } }, required: ['prompt'] } } , { name: 'generate_video', description: `Generate short videos using AIHUBMIX VEO-3 (OpenAI-compatible chat endpoint). Supports text-to-video and two-frame transition (image to video). Returns async links; in file mode will download the resulting MP4.`, inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'Describe the desired video (e.g., duration, style, motion).' }, inputs: { type: 'array', items: { type: 'object', properties: { type: { type: 'string', enum: ['image', 'video'], description: 'Input media type' }, data: { type: 'string', description: 'Local path, HTTP/HTTPS URL, base64, or data URL (<=20MB).' }, mimeType: { type: 'string', description: 'MIME type (e.g., image/png, video/mp4)' } }, required: ['type', 'data'] }, description: 'Optional media array. Typical use: two images as start/end frames.' }, output: { type: 'object', description: 'Output options', properties: { mode: { type: 'string', enum: ['inlineBase64', 'summary', 'file'], description: 'How to return the result', default: 'file' }, previewChars: { type: 'number', description: 'For summary mode: length of text preview', default: 200 }, dir: { type: 'string', description: 'For file mode: directory to save MP4 (auto-created)', default: './generated-videos' }, filename: { type: 'string', description: 'For file mode: filename (default ai-video-<timestamp>.mp4)' } } } }, required: ['prompt'] } } ] })); // Handle tool calls server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'generate_image') { try { const { prompt, images, output } = args; const imageBase64 = await generateImage(prompt, images); // Fix for CodeBuddy compatibility: return pure base64 instead of data URL const pureBase64 = imageBase64.replace(/^data:image\/[^;]+;base64,/, ''); const mode = (output && output.mode) || process.env.AIHUBMIX_OUTPUT_MODE || 'file'; // New: url mode via Supabase Storage if (mode === 'url') { const SUPABASE_BUCKET = process.env.SUPABASE_BUCKET || 'MATERIALS'; const PREFIX = (process.env.SUPABASE_PATH_PREFIX || 'mcp/').replace(/^\/+|\/+$/g, '') + '/'; const ts = new Date().toISOString().replace(/[:.]/g, '-'); const fname = (output && output.filename) || `ai-image-${ts}.png`; const objectKey = `${PREFIX}${fname}`; const mimeType = 'image/png'; const buf = Buffer.from(pureBase64, 'base64'); if (!supabase) { // fall back to file if client not ready const warn = 'Supabase client not configured; falling back to file mode.'; const outDir = (output && output.dir) || process.env.AIHUBMIX_OUTPUT_DIR || './generated-images'; const cwd = process.cwd && process.cwd(); const baseDir = (cwd && cwd !== '/') ? cwd : path.join(__dirname, '..'); const fullDir = path.isAbsolute(outDir) ? outDir : path.join(baseDir, outDir); const fullPath = path.join(fullDir, fname); try { if (!fs.existsSync(fullDir)) fs.mkdirSync(fullDir, { recursive: true }); fs.writeFileSync(fullPath, buf); const bytes = Buffer.byteLength(buf); return { content: [{ type: 'text', text: `Image generated (fallback file). ${warn} path=${fullPath}, bytes=${bytes}` }] }; } catch (e) { return { content: [{ type: 'text', text: `Error saving image (fallback file): ${e.message}` }], isError: true }; } } try { const { data, error } = await supabase .storage .from(SUPABASE_BUCKET) .upload(objectKey, buf, { contentType: mimeType, upsert: true }); if (error) throw error; // public URL const { data: pub } = supabase.storage.from(SUPABASE_BUCKET).getPublicUrl(objectKey); const url = pub && pub.publicUrl ? pub.publicUrl : ''; if (!url) throw new Error('Failed to resolve public URL'); return { content: [ { type: 'text', text: `Image generated successfully for prompt: "${prompt}" (mode=url, url=${url})` } ] }; } catch (e) { // upload failed -> fallback to file const warn = `Supabase upload failed: ${e.message}. Falling back to file mode.`; const outDir = (output && output.dir) || process.env.AIHUBMIX_OUTPUT_DIR || './generated-images'; const cwd = process.cwd && process.cwd(); const baseDir = (cwd && cwd !== '/') ? cwd : path.join(__dirname, '..'); const fullDir = path.isAbsolute(outDir) ? outDir : path.join(baseDir, outDir); const fullPath = path.join(fullDir, fname); try { if (!fs.existsSync(fullDir)) fs.mkdirSync(fullDir, { recursive: true }); fs.writeFileSync(fullPath, buf); const bytes = Buffer.byteLength(buf); return { content: [{ type: 'text', text: `Image generated (fallback file). ${warn} path=${fullPath}, bytes=${bytes}` }] }; } catch (e2) { return { content: [{ type: 'text', text: `Error saving image to file after upload failure: ${e2.message}` }], isError: true }; } } } if (mode === 'file') { const outDir = (output && output.dir) || process.env.AIHUBMIX_OUTPUT_DIR || './generated-images'; const ts = new Date().toISOString().replace(/[:.]/g, '-'); const fname = (output && output.filename) || `ai-image-${ts}.png`; const cwd = process.cwd && process.cwd(); const baseDir = (cwd && cwd !== '/') ? cwd : path.join(__dirname, '..'); const fullDir = path.isAbsolute(outDir) ? outDir : path.join(baseDir, outDir); const fullPath = path.join(fullDir, fname); try { if (!fs.existsSync(fullDir)) fs.mkdirSync(fullDir, { recursive: true }); fs.writeFileSync(fullPath, Buffer.from(pureBase64, 'base64')); const bytes = Buffer.byteLength(pureBase64, 'base64'); return { content: [ { type: 'text', text: `Image generated successfully for prompt: "${prompt}" (mode=file, path=${fullPath}, bytes=${bytes})` } ] }; } catch (e) { return { content: [ { type: 'text', text: `Error saving image to file: ${e.message}` } ], isError: true }; } } if (mode === 'summary') { const n = Math.max(0, Math.min(10000, (output && output.previewChars) != null ? output.previewChars : 80)); const preview = pureBase64.slice(0, n); return { content: [ { type: 'text', text: `Image generated successfully for prompt: "${prompt}" (mode=summary, base64Length=${pureBase64.length}, preview="${preview}...")` } ] }; } return { content: [ { type: 'text', text: `Image generated successfully for prompt: "${prompt}" (mode=inlineBase64, base64Length=${pureBase64.length})` }, { type: 'image', data: pureBase64, mimeType: 'image/png' } ] }; } catch (error) { return { content: [ { type: 'text', text: `Error generating image: ${error.message}` } ], isError: true }; } } if (name === 'generate_video') { try { const { prompt, inputs, output } = args; const { text, urls } = await generateVideo(prompt, inputs); const mode = (output && output.mode) || process.env.AIHUBMIX_OUTPUT_MODE || 'file'; // Try to save mp4 if possible if (mode === 'file') { const outDir = (output && output.dir) || process.env.AIHUBMIX_OUTPUT_DIR || './generated-videos'; const ts = new Date().toISOString().replace(/[:.]/g, '-'); const fname = (output && output.filename) || `ai-video-${ts}.mp4`; const cwd = process.cwd && process.cwd(); const baseDir = (cwd && cwd !== '/') ? cwd : path.join(__dirname, '..'); const fullDir = path.isAbsolute(outDir) ? outDir : path.join(baseDir, outDir); if (!fs.existsSync(fullDir)) fs.mkdirSync(fullDir, { recursive: true }); let chosenUrl = Array.isArray(urls) ? (urls.find(u => u.toLowerCase().includes('.mp4')) || null) : null; if (chosenUrl) { try { const res = await axios_1.default.get(chosenUrl, { responseType: 'arraybuffer', timeout: Math.max(180000, config.timeout) }); const ct = ((res.headers && (res.headers['content-type'] || res.headers['Content-Type'])) || '').toLowerCase(); if (!ct.includes('mp4')) { throw new Error(`Unexpected content-type when downloading video: ${ct || 'unknown'}`); } const fullPath = path.join(fullDir, fname); fs.writeFileSync(fullPath, Buffer.from(res.data)); const bytes = Buffer.byteLength(res.data); return { content: [ { type: 'text', text: `Video generated successfully for prompt: "${prompt}" (mode=file, path=${fullPath}, bytes=${bytes})\n${text}` } ] }; } catch (e) { // fallback to summary with links return { content: [ { type: 'text', text: `Video generated (download failed): ${e.message}\nLinks:\n${(urls||[]).join('\n')}\n\n${text}` } ] }; } } else { // No URLs detected — return summary return { content: [ { type: 'text', text: `Video task created but no direct URLs found yet.\n${text}` } ] }; } } if (mode === 'summary') { const n = Math.max(0, Math.min(5000, (output && output.previewChars) != null ? output.previewChars : 200)); const snippet = text.slice(0, n); const links = (urls && urls.length) ? `\nLinks:\n${urls.join('\n')}` : ''; return { content: [{ type: 'text', text: `Video generation summary for "${prompt}":\n${snippet}${snippet.length < text.length ? '...' : ''}${links}` }] }; } // inlineBase64 is not practical for video; return text+links const links = (urls && urls.length) ? `\nLinks:\n${urls.join('\n')}` : ''; return { content: [{ type: 'text', text: `Video generation result (text):\n${text}${links}` }] }; } catch (error) { return { content: [{ type: 'text', text: `Error generating video: ${error.message}` }], isError: true }; } } throw new Error(`Unknown tool: ${name}`); }); // Start server async function main() { const transport = new stdio_js_1.StdioServerTransport(); await server.connect(transport); console.error('AIHUBMIX Image MCP Server running'); } if (require.main === module) { main().catch(console.error); } //# sourceMappingURL=index.js.map