generate_image
Generate AI images using MeiGen, local ComfyUI, or OpenAI APIs. Accepts prompts, reference images, and custom settings for style and aspect ratio.
Instructions
Generate an image using AI. Supports MeiGen platform, local ComfyUI, or OpenAI-compatible APIs. Tip: get prompts from get_inspiration() or enhance_prompt(), and use gallery image URLs as referenceImages for style guidance. For Midjourney V8.1, an optional style reference can be passed by appending --sref <code> at the end of the prompt — only when the user provides a Midjourney style code (numeric or text). Do NOT pass URLs or local paths via --sref; for any image-based reference, use the referenceImages parameter instead.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| prompt | Yes | The image generation prompt | |
| model | No | Model name. For OpenAI-compatible providers: any model ID your endpoint supports. For MeiGen: use model IDs from list_models. | |
| size | No | Image size for OpenAI-compatible providers: "1024x1024", "1536x1024", "auto". MeiGen/ComfyUI: use aspectRatio instead. | |
| aspectRatio | No | Aspect ratio for MeiGen provider. Use "auto" (recommended, default when omitted) to let MeiGen infer the best ratio from the prompt content. Explicit values: "1:1", "3:4", "4:3", "16:9", "9:16", "21:9", "2:3", "3:2", "4:5", "5:4", etc. (model-dependent). ComfyUI: use comfyui_workflow modify to adjust dimensions before generating. | |
| resolution | No | Resolution tier. MeiGen: "1K" / "2K" / "3K" / "4K" — each model supports a subset (list_models reports resolutions when applicable). OpenAI: not used (use size instead). | |
| quality | No | Image quality. MeiGen gpt-image-2: "low" / "medium" / "high". OpenAI-compatible providers also accept "high". | |
| referenceImages | No | Image references for style/content guidance. Accepts both public URLs (http/https) and local file paths. Local files are automatically compressed and uploaded when needed. For ComfyUI: local files are passed directly to the workflow (requires LoadImage node). Sources: gallery URLs from search_gallery/get_inspiration, URLs from previous generate_image results, or local file paths. | |
| provider | No | Which provider to use. Auto-detected from configuration if not specified. | |
| workflow | No | ComfyUI workflow name to use (from comfyui_workflow list). Uses default workflow if not specified. | |
| negativePrompt | No | Negative prompt for OpenAI-compatible providers. ComfyUI: use comfyui_workflow modify to set negative prompt in the workflow before generating. |
Implementation Reference
- src/tools/generate-image.ts:1-376 (handler)Main implementation of the generate_image tool. Contains the core handler logic with three provider modes (MeiGen, OpenAI-compatible, ComfyUI), the registerGenerateImage registration function, provider-specific generation functions (generateWithOpenAI, generateWithMeiGen, generateWithComfyUI), helper functions (notify, isLocalPath, resolveLocalPath, resolveReferenceImages), the generateImageSchema, and all supporting logic.
/** * generate_image Tool — requires authentication, three provider modes: * Mode A: MeiGen account -> calls MeiGen platform API * Mode B: ComfyUI local -> submits workflow to local ComfyUI * Mode C: User's own API key -> calls OpenAI-compatible API */ import { z } from 'zod' import { existsSync } from 'fs' import { homedir } from 'os' import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js' import type { ServerRequest, ServerNotification } from '@modelcontextprotocol/sdk/types.js' import type { MeiGenConfig, ProviderType } from '../config.js' import { getDefaultProvider, getAvailableProviders } from '../config.js' import type { MeiGenApiClient } from '../lib/meigen-api.js' import { OpenAIProvider } from '../lib/providers/openai.js' import { ComfyUIProvider, loadWorkflow, listWorkflows, } from '../lib/providers/comfyui.js' import { sharedApiSemaphore, classifyError } from '../lib/generation-shared.js' import { Semaphore } from '../lib/semaphore.js' import { saveImageLocally } from '../lib/save-image.js' import { addRecentGeneration } from '../lib/preferences.js' import { processAndUploadImage } from '../lib/upload.js' import { unsafeReferenceUrlReason } from '../lib/url-safety.js' // MCP 不再硬编码 MeiGen 默认模型。 // 用户不传 model 时,MeiGen 后端会按 DB is_default=true 的行决定, // 响应里回传实际使用的 modelId,MCP 据此展示给用户。 // 好处: 后端切默认(比如 gpt-image-2 维护/恢复)不需要发 npm 版本。 // API semaphore: shared with generate_video (same backend endpoint, same 12/min rate limit). // ComfyUI: serial (local GPU constraint). const comfyuiSemaphore = new Semaphore(1) /** Safe notification — silently ignores if client doesn't support logging */ async function notify(extra: RequestHandlerExtra<ServerRequest, ServerNotification>, message: string) { try { await extra.sendNotification({ method: 'notifications/message', params: { level: 'info', logger: 'generate_image', data: message }, }) } catch { // Client doesn't support logging — ignore } } /** Check if a string looks like a local file path (not a URL) */ function isLocalPath(ref: string): boolean { if (ref.startsWith('http://') || ref.startsWith('https://')) return false if (ref.startsWith('file://')) return true return ref.startsWith('/') || ref.startsWith('~') || /^[A-Z]:[/\\]/i.test(ref) } /** Resolve file:// URIs and ~ prefix to absolute paths */ function resolveLocalPath(ref: string): string { if (ref.startsWith('file://')) return ref.slice(7) if (ref.startsWith('~')) return homedir() + ref.slice(1) return ref } /** * Resolve local file paths in referenceImages to public URLs by uploading them. * URLs are passed through unchanged. ComfyUI is skipped (handles local files natively). */ async function resolveReferenceImages( refs: string[] | undefined, config: MeiGenConfig, notifyFn: (msg: string) => Promise<void>, ): Promise<string[] | undefined> { if (!refs || refs.length === 0) return refs return Promise.all(refs.map(async (ref) => { if (!isLocalPath(ref)) { // Defense-in-depth: reject obviously-unsafe URLs (file://, data:, private IPs, // cloud metadata) before relaying to the backend. Backend SHOULD also filter // but this saves a network round-trip and gives users a clearer error. const unsafe = unsafeReferenceUrlReason(ref) if (unsafe) { throw new Error(`Reference image URL rejected: ${unsafe}. URL: ${ref}`) } return ref } const filePath = resolveLocalPath(ref) if (!existsSync(filePath)) { throw new Error(`Reference image not found: ${filePath}`) } await notifyFn(`Uploading reference image: ${filePath}...`) const result = await processAndUploadImage(filePath, config) return result.publicUrl })) } export const generateImageSchema = { prompt: z.string().trim().min(1, 'Prompt cannot be empty').describe('The image generation prompt'), model: z.string().optional() .describe('Model name. For OpenAI-compatible providers: any model ID your endpoint supports. For MeiGen: use model IDs from list_models.'), size: z.string().optional() .describe('Image size for OpenAI-compatible providers: "1024x1024", "1536x1024", "auto". MeiGen/ComfyUI: use aspectRatio instead.'), aspectRatio: z.string().optional() .describe('Aspect ratio for MeiGen provider. Use "auto" (recommended, default when omitted) to let MeiGen infer the best ratio from the prompt content. Explicit values: "1:1", "3:4", "4:3", "16:9", "9:16", "21:9", "2:3", "3:2", "4:5", "5:4", etc. (model-dependent). ComfyUI: use comfyui_workflow modify to adjust dimensions before generating.'), resolution: z.string().optional() .describe('Resolution tier. MeiGen: "1K" / "2K" / "3K" / "4K" — each model supports a subset (list_models reports resolutions when applicable). OpenAI: not used (use size instead).'), quality: z.string().optional() .describe('Image quality. MeiGen gpt-image-2: "low" / "medium" / "high". OpenAI-compatible providers also accept "high".'), referenceImages: z.array(z.string()).optional() .describe('Image references for style/content guidance. Accepts both public URLs (http/https) and local file paths. Local files are automatically compressed and uploaded when needed. For ComfyUI: local files are passed directly to the workflow (requires LoadImage node). Sources: gallery URLs from search_gallery/get_inspiration, URLs from previous generate_image results, or local file paths.'), provider: z.enum(['openai', 'meigen', 'comfyui']).optional() .describe('Which provider to use. Auto-detected from configuration if not specified.'), workflow: z.string().optional() .describe('ComfyUI workflow name to use (from comfyui_workflow list). Uses default workflow if not specified.'), negativePrompt: z.string().optional() .describe('Negative prompt for OpenAI-compatible providers. ComfyUI: use comfyui_workflow modify to set negative prompt in the workflow before generating.'), } export function registerGenerateImage(server: McpServer, apiClient: MeiGenApiClient, config: MeiGenConfig) { server.tool( 'generate_image', 'Generate an image using AI. Supports MeiGen platform, local ComfyUI, or OpenAI-compatible APIs. Tip: get prompts from get_inspiration() or enhance_prompt(), and use gallery image URLs as referenceImages for style guidance. For Midjourney V8.1, an optional style reference can be passed by appending `--sref <code>` at the end of the prompt — only when the user provides a Midjourney style code (numeric or text). Do NOT pass URLs or local paths via --sref; for any image-based reference, use the referenceImages parameter instead.', generateImageSchema, { readOnlyHint: false, destructiveHint: true }, async ({ prompt, model, size, aspectRatio, resolution, quality, referenceImages, provider: requestedProvider, workflow, negativePrompt }, extra) => { const availableProviders = getAvailableProviders(config) if (availableProviders.length === 0) { return { content: [{ type: 'text' as const, text: 'No image generation providers configured. Get a MeiGen API token at https://www.meigen.ai (sign in → Settings → API Keys), then set MEIGEN_API_TOKEN in your environment or MCP config and restart the host. Claude Code users can run /meigen:setup for guided configuration. Alternative providers: OPENAI_API_KEY (any OpenAI-compatible API) or ComfyUI workflow import.', }], isError: true, } } // Determine which provider to use let providerType: ProviderType if (requestedProvider) { if (!availableProviders.includes(requestedProvider)) { return { content: [{ type: 'text' as const, text: `Provider "${requestedProvider}" is not configured. Available: ${availableProviders.join(', ')}`, }], isError: true, } } providerType = requestedProvider } else { providerType = getDefaultProvider(config)! } try { // Auto-upload local reference images for API providers (ComfyUI handles local files natively) const resolvedRefs = providerType !== 'comfyui' ? await resolveReferenceImages(referenceImages, config, (msg) => notify(extra, msg)) : referenceImages switch (providerType) { case 'openai': { await sharedApiSemaphore.acquire() try { return await generateWithOpenAI(config, prompt, model, size, quality, resolvedRefs) } finally { sharedApiSemaphore.release() } } case 'meigen': { await sharedApiSemaphore.acquire() try { return await generateWithMeiGen(apiClient, prompt, model, aspectRatio, resolution, quality, resolvedRefs, extra) } finally { sharedApiSemaphore.release() } } case 'comfyui': { await comfyuiSemaphore.acquire() try { return await generateWithComfyUI(config, prompt, workflow, referenceImages, extra) } finally { comfyuiSemaphore.release() } } default: return { content: [{ type: 'text' as const, text: `Unknown provider: ${providerType}` }], isError: true, } } } catch (error) { const message = error instanceof Error ? error.message : String(error) const guidance = classifyError(message) return { content: [{ type: 'text' as const, text: `Image generation failed: ${message}\n\n${guidance}`, }], isError: true, } } } ) } // ============================================================ // Provider-specific generation functions // ============================================================ async function generateWithOpenAI( config: MeiGenConfig, prompt: string, model?: string, size?: string, quality?: string, referenceImages?: string[], ) { const provider = new OpenAIProvider(config.openaiApiKey!, config.openaiBaseUrl, config.openaiModel) const result = await provider.generate({ prompt, model, size, quality, referenceImages }) const savedPath = saveImageLocally(result.imageBase64, result.mimeType) addRecentGeneration({ prompt, provider: 'openai', model: model || config.openaiModel }) const lines = [`Image generated successfully.`] lines.push(`- Provider: OpenAI-compatible (${model || config.openaiModel})`) if (referenceImages?.length) lines.push(`- Reference images: ${referenceImages.length} used`) if (savedPath) lines.push(`- Saved to: ${savedPath}`) return { content: [{ type: 'text' as const, text: lines.join('\n') }], } } async function generateWithMeiGen( apiClient: MeiGenApiClient, prompt: string, model: string | undefined, aspectRatio: string | undefined, resolution: string | undefined, quality: string | undefined, referenceImages: string[] | undefined, extra: RequestHandlerExtra<ServerRequest, ServerNotification>, ) { // 1. Submit generation request // model / resolution / quality 不强制填充默认值;缺省时由 MeiGen 后端按 DB 决定 const genResponse = await apiClient.generateImage({ prompt, modelId: model, aspectRatio: aspectRatio || 'auto', resolution, quality, referenceImages, }) if (!genResponse.generationId) { throw new Error('No generation ID returned') } // Notify: generation submitted await notify(extra, 'Image generation submitted, waiting for result...') // 2. Poll until completed (with progress notifications) const status = await apiClient.waitForGeneration( genResponse.generationId, 300_000, async (elapsedMs) => { await notify(extra, `Still generating... (${Math.round(elapsedMs / 1000)}s elapsed)`) }, ) if (status.status === 'failed') { throw new Error(status.error || 'Generation failed') } // Detect video model misuse early — give a clear redirect instead of cryptic "no image URL" if (status.mediaType === 'video') { throw new Error('This model produces video. Use the generate_video tool with the same model id.') } // Use imageUrls array if available (e.g., V8.1 returns 4 candidates), fall back to imageUrl const allImageUrls = status.imageUrls?.length ? status.imageUrls : (status.imageUrl ? [status.imageUrl] : []) if (allImageUrls.length === 0) { throw new Error('No image URL in completed generation') } // Download first image for local save const imageRes = await fetch(allImageUrls[0]) if (!imageRes.ok) { throw new Error(`Failed to download generated image: ${imageRes.status}`) } const buffer = await imageRes.arrayBuffer() const base64 = Buffer.from(buffer).toString('base64') const mimeType = imageRes.headers.get('content-type') || 'image/jpeg' const savedPath = saveImageLocally(base64, mimeType) // 优先用后端返回的 modelId(反映真实使用的模型,包含 is_default 解析结果); // 若后端未回传(旧版 backend),用用户显式传入的 model,再 fallback 到占位 const actualModel = genResponse.modelId || model || 'meigen-default' addRecentGeneration({ prompt, provider: 'meigen', model: actualModel, aspectRatio }) const lines = [`Image generated successfully.`] lines.push(`- Provider: MeiGen (model: ${actualModel})`) if (allImageUrls.length > 1) { lines.push(`- ${allImageUrls.length} candidate images returned:`) allImageUrls.forEach((url, i) => lines.push(` ${i + 1}. ${url}`)) } else { lines.push(`- Image URL: ${allImageUrls[0]}`) } if (savedPath) lines.push(`- Saved to: ${savedPath}`) lines.push(`\nYou can use any Image URL as referenceImages for follow-up generation.`) return { content: [{ type: 'text' as const, text: lines.join('\n') }], } } async function generateWithComfyUI( config: MeiGenConfig, prompt: string, workflow: string | undefined, referenceImages: string[] | undefined, extra: RequestHandlerExtra<ServerRequest, ServerNotification>, ) { // Determine workflow const workflows = listWorkflows() if (workflows.length === 0) { throw new Error('No ComfyUI workflows configured. Use comfyui_workflow import to add one (or on Claude Code, run /meigen:setup for guided configuration).') } const workflowName = workflow || config.comfyuiDefaultWorkflow || workflows[0] const workflowData = loadWorkflow(workflowName) const comfyuiUrl = config.comfyuiUrl || 'http://localhost:8188' const provider = new ComfyUIProvider(comfyuiUrl) // Pre-flight: check if ComfyUI is reachable const health = await provider.checkConnection() if (!health.ok) { throw new Error(`ComfyUI is not reachable at ${comfyuiUrl}. Make sure ComfyUI is running.\nDetails: ${health.error}`) } // Notify: generation submitted await notify(extra, `Submitting workflow "${workflowName}" to ComfyUI...`) const result = await provider.generate( workflowData, prompt, { referenceImages }, async (elapsedMs) => { await notify(extra, `Still generating... (${Math.round(elapsedMs / 1000)}s elapsed)`) }, ) const savedPath = saveImageLocally(result.imageBase64, result.mimeType) addRecentGeneration({ prompt, provider: 'comfyui', model: workflowName }) const lines = [`Image generated successfully.`] lines.push(`- Provider: ComfyUI (workflow: ${workflowName})`) if (savedPath) lines.push(`- Saved to: ${savedPath}`) if (result.referenceImageWarning) lines.push(`\nWarning: ${result.referenceImageWarning}`) return { content: [{ type: 'text' as const, text: lines.join('\n') }], } } - src/tools/generate-image.ts:99-119 (schema)generateImageSchema — Zod validation schema defining all input parameters: prompt (required string), model, size, aspectRatio, resolution, quality, referenceImages (array of URLs or local paths), provider (enum: openai/meigen/comfyui), workflow, and negativePrompt.
export const generateImageSchema = { prompt: z.string().trim().min(1, 'Prompt cannot be empty').describe('The image generation prompt'), model: z.string().optional() .describe('Model name. For OpenAI-compatible providers: any model ID your endpoint supports. For MeiGen: use model IDs from list_models.'), size: z.string().optional() .describe('Image size for OpenAI-compatible providers: "1024x1024", "1536x1024", "auto". MeiGen/ComfyUI: use aspectRatio instead.'), aspectRatio: z.string().optional() .describe('Aspect ratio for MeiGen provider. Use "auto" (recommended, default when omitted) to let MeiGen infer the best ratio from the prompt content. Explicit values: "1:1", "3:4", "4:3", "16:9", "9:16", "21:9", "2:3", "3:2", "4:5", "5:4", etc. (model-dependent). ComfyUI: use comfyui_workflow modify to adjust dimensions before generating.'), resolution: z.string().optional() .describe('Resolution tier. MeiGen: "1K" / "2K" / "3K" / "4K" — each model supports a subset (list_models reports resolutions when applicable). OpenAI: not used (use size instead).'), quality: z.string().optional() .describe('Image quality. MeiGen gpt-image-2: "low" / "medium" / "high". OpenAI-compatible providers also accept "high".'), referenceImages: z.array(z.string()).optional() .describe('Image references for style/content guidance. Accepts both public URLs (http/https) and local file paths. Local files are automatically compressed and uploaded when needed. For ComfyUI: local files are passed directly to the workflow (requires LoadImage node). Sources: gallery URLs from search_gallery/get_inspiration, URLs from previous generate_image results, or local file paths.'), provider: z.enum(['openai', 'meigen', 'comfyui']).optional() .describe('Which provider to use. Auto-detected from configuration if not specified.'), workflow: z.string().optional() .describe('ComfyUI workflow name to use (from comfyui_workflow list). Uses default workflow if not specified.'), negativePrompt: z.string().optional() .describe('Negative prompt for OpenAI-compatible providers. ComfyUI: use comfyui_workflow modify to set negative prompt in the workflow before generating.'), } - src/tools/generate-image.ts:121-207 (registration)registerGenerateImage function that registers the 'generate_image' tool on the MCP server via server.tool(). Maps the schema and handler, dispatches to provider-specific logic based on user selection or auto-detection.
export function registerGenerateImage(server: McpServer, apiClient: MeiGenApiClient, config: MeiGenConfig) { server.tool( 'generate_image', 'Generate an image using AI. Supports MeiGen platform, local ComfyUI, or OpenAI-compatible APIs. Tip: get prompts from get_inspiration() or enhance_prompt(), and use gallery image URLs as referenceImages for style guidance. For Midjourney V8.1, an optional style reference can be passed by appending `--sref <code>` at the end of the prompt — only when the user provides a Midjourney style code (numeric or text). Do NOT pass URLs or local paths via --sref; for any image-based reference, use the referenceImages parameter instead.', generateImageSchema, { readOnlyHint: false, destructiveHint: true }, async ({ prompt, model, size, aspectRatio, resolution, quality, referenceImages, provider: requestedProvider, workflow, negativePrompt }, extra) => { const availableProviders = getAvailableProviders(config) if (availableProviders.length === 0) { return { content: [{ type: 'text' as const, text: 'No image generation providers configured. Get a MeiGen API token at https://www.meigen.ai (sign in → Settings → API Keys), then set MEIGEN_API_TOKEN in your environment or MCP config and restart the host. Claude Code users can run /meigen:setup for guided configuration. Alternative providers: OPENAI_API_KEY (any OpenAI-compatible API) or ComfyUI workflow import.', }], isError: true, } } // Determine which provider to use let providerType: ProviderType if (requestedProvider) { if (!availableProviders.includes(requestedProvider)) { return { content: [{ type: 'text' as const, text: `Provider "${requestedProvider}" is not configured. Available: ${availableProviders.join(', ')}`, }], isError: true, } } providerType = requestedProvider } else { providerType = getDefaultProvider(config)! } try { // Auto-upload local reference images for API providers (ComfyUI handles local files natively) const resolvedRefs = providerType !== 'comfyui' ? await resolveReferenceImages(referenceImages, config, (msg) => notify(extra, msg)) : referenceImages switch (providerType) { case 'openai': { await sharedApiSemaphore.acquire() try { return await generateWithOpenAI(config, prompt, model, size, quality, resolvedRefs) } finally { sharedApiSemaphore.release() } } case 'meigen': { await sharedApiSemaphore.acquire() try { return await generateWithMeiGen(apiClient, prompt, model, aspectRatio, resolution, quality, resolvedRefs, extra) } finally { sharedApiSemaphore.release() } } case 'comfyui': { await comfyuiSemaphore.acquire() try { return await generateWithComfyUI(config, prompt, workflow, referenceImages, extra) } finally { comfyuiSemaphore.release() } } default: return { content: [{ type: 'text' as const, text: `Unknown provider: ${providerType}` }], isError: true, } } } catch (error) { const message = error instanceof Error ? error.message : String(error) const guidance = classifyError(message) return { content: [{ type: 'text' as const, text: `Image generation failed: ${message}\n\n${guidance}`, }], isError: true, } } } ) } - src/server.ts:13-13 (registration)Import of registerGenerateImage from the generate-image module in the server.
import { registerGenerateImage } from './tools/generate-image.js' - src/server.ts:275-275 (registration)Call to registerGenerateImage(server, apiClient, config) which registers the generate_image tool on the MCP server.
registerGenerateImage(server, apiClient, config)