aihubmix-image-mcp
Version:
MCP server for AIHUBMIX image generation with URL support (NANO BANANA)
872 lines (862 loc) • 41.4 kB
JavaScript
;
/**
* 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