generate_blog_image
Generates AI images for blog posts and social media platforms with preset sizes for Ghost, Medium, Instagram, Twitter, LinkedIn, YouTube, and more.
Instructions
Generate images for blog posts and social media using AI.
Available formats:
ghost-banner: Ghost Blog Banner (1200x675)
ghost-feature: Ghost Feature Image (HD) (2000x1125)
medium-ghost-spooky: Medium Ghost Spooky (2560x1440)
medium-banner: Medium Banner (1400x788)
substack-header: Substack Header (1456x816)
wordpress-featured: WordPress Featured (1200x675)
instagram-post: Instagram Post (1080x1080)
instagram-story: Instagram Story (1080x1920)
instagram-landscape: Instagram Landscape (1080x608)
twitter-post: Twitter/X Post (1200x675)
twitter-header: Twitter/X Header (1500x500)
linkedin-post: LinkedIn Post (1200x628)
linkedin-banner: LinkedIn Banner (1584x396)
facebook-post: Facebook Post (1200x630)
facebook-cover: Facebook Cover (820x312)
youtube-thumbnail: YouTube Thumbnail (1280x720)
youtube-banner: YouTube Banner (2560x1440)
square: Square (1024x1024)
square-hd: Square HD (2048x2048)
landscape: Landscape (1920x1080)
landscape-4k: Landscape 4K (3840x2160)
portrait: Portrait (1080x1920)
Examples:
Generate a Ghost blog banner: { "prompt": "A serene mountain landscape at sunset", "format": "ghost-banner" }
High quality Instagram post: { "prompt": "Minimalist coffee cup on marble", "format": "instagram-post", "quality": "high" }
YouTube thumbnail with title: { "prompt": "Exciting tech reveal", "format": "youtube-thumbnail", "title": "New iPhone 17 Review" }
IMPORTANT: Always specify an outputPath to save the image to a meaningful location. If omitted, images are saved to a generated-images/ directory in the current working directory with a timestamped filename.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| prompt | Yes | Description of the image to generate | |
| format | No | Platform preset: ghost-banner, ghost-feature, medium-ghost-spooky, medium-banner, substack-header, wordpress-featured, instagram-post, instagram-story, instagram-landscape, twitter-post, twitter-header, linkedin-post, linkedin-banner, facebook-post, facebook-cover, youtube-thumbnail, youtube-banner, square, square-hd, landscape, landscape-4k, portrait | ghost-banner |
| quality | No | Quality level | standard |
| style | No | Optional style hint (e.g., 'photorealistic', 'illustration') | |
| title | No | Optional blog post title for context | |
| outputPath | No | Optional path to save the image file | |
| provider | No | Image generation provider | gemini |
Implementation Reference
- src/tools/generate-image.ts:82-226 (handler)The main execution function for the generate_blog_image tool. Validates prompt and output path, resolves the platform preset, calls the provider to generate the image, saves it to disk with embedded metadata, and returns the result with dimensions, file size, and warnings.
export async function executeGenerateImage( input: GenerateImageInput ): Promise<GenerateImageResult> { // Validate prompt const promptValidation = validatePrompt(input.prompt); if (!promptValidation.valid) { return { success: false, message: "Invalid prompt", error: promptValidation.error, }; } // Validate output path if provided if (input.outputPath) { const pathValidation = validateOutputPath(input.outputPath); if (!pathValidation.valid) { return { success: false, message: "Invalid output path", error: pathValidation.error, }; } } // Get preset configuration const preset = PLATFORM_PRESETS[input.format]; if (!preset) { return { success: false, message: "Invalid format", error: `Unknown format: ${input.format}. Available: ${PRESET_KEYS.join(", ")}`, }; } // Create provider const provider = createProvider(input.provider as ProviderName); if (!provider.isConfigured()) { return { success: false, message: "Provider not configured", error: `${input.provider} provider requires API key. Set GOOGLE_API_KEY environment variable.`, }; } // Build enhanced prompt with title context let enhancedPrompt = promptValidation.sanitized!; if (input.title) { enhancedPrompt = `For a blog post titled "${input.title}": ${enhancedPrompt}`; } // Generate image let generatedImage: GeneratedImage; try { generatedImage = await provider.generateImage({ prompt: enhancedPrompt, aspectRatio: preset.aspectRatio, width: preset.width, height: preset.height, quality: input.quality, style: input.style, }); } catch (error) { return { success: false, message: "Image generation failed", error: safeErrorMessage(error), }; } // Calculate file size const fileSize = formatFileSize(estimateFileSize(generatedImage.base64Data)); // Determine output path - use provided path or generate a default const outputPath = input.outputPath ?? generateDefaultOutputPath(generatedImage.mimeType); // Build metadata to embed in the image const metadata: ImageMetadata = { prompt: input.prompt, model: generatedImage.model ?? "gemini-2.0-flash-exp", provider: input.provider, format: input.format, style: input.style, title: input.title, generatedAt: new Date().toISOString(), }; // Always save the image to ensure it's never lost let savedPath: string; try { savedPath = await saveImage( generatedImage.base64Data, outputPath, generatedImage.mimeType, metadata ); } catch (error) { return { success: false, message: "Failed to save image", error: safeErrorMessage(error), }; } // Determine actual dimensions (from provider or estimate from preset) const actualWidth = generatedImage.width ?? preset.width; const actualHeight = generatedImage.height ?? preset.height; // Build warning message if aspect ratio is not natively supported let warning: string | undefined; if (!preset.nativeAspectRatio) { warning = `Note: This preset's aspect ratio (${preset.aspectRatio}) is not natively supported by Gemini. ` + `Image was generated at ${preset.geminiAspectRatio} aspect ratio. ` + `You may need to crop the image to fit ${preset.width}x${preset.height}.`; } else if (actualWidth !== preset.width || actualHeight !== preset.height) { warning = `Image was generated at ${actualWidth}x${actualHeight}, which differs from the preset's ${preset.width}x${preset.height}. ` + `You may need to resize the image.`; } // Build descriptive message - image is always saved const message = `Image generated and saved to ${savedPath}`; return { success: true, message, image: { base64Data: generatedImage.base64Data, mimeType: generatedImage.mimeType, format: input.format, dimensions: { width: actualWidth, height: actualHeight, requestedWidth: preset.width, requestedHeight: preset.height, }, fileSize, savedTo: savedPath, }, preset, warning, }; } - src/tools/generate-image.ts:21-51 (schema)Zod schema defining the input validation for generate_blog_image. Validates prompt (3-4000 chars), format (platform preset enum), quality (standard/high), optional style and title, outputPath, and provider (currently only 'gemini').
export const GenerateImageInputSchema = z.object({ prompt: z .string() .min(3, "Prompt must be at least 3 characters") .max(4000, "Prompt must be less than 4000 characters") .describe("Description of the image to generate"), format: z .enum(PRESET_KEYS as [string, ...string[]]) .default("ghost-banner") .describe("Platform preset (e.g., 'ghost-banner', 'instagram-post', 'twitter-post')"), quality: z .enum(["standard", "high"]) .default("standard") .describe("Quality level: 'standard' (faster) or 'high' (better quality, uses Pro model)"), style: z .string() .max(200) .optional() .describe("Optional style hint (e.g., 'photorealistic', 'illustration', 'minimalist')"), title: z.string().max(200).optional().describe("Optional blog post title for context"), outputPath: z.string().max(500).optional().describe("Optional path to save the image file"), provider: z.enum(["gemini"]).default("gemini").describe("Image generation provider to use"), }); export type GenerateImageInput = z.infer<typeof GenerateImageInputSchema>; - src/index.ts:46-95 (registration)Tool registration in the MCP ListTools handler. Registers the tool named 'generate_blog_image' with its description, input schema properties (prompt, format, quality, style, title, outputPath, provider), and required fields.
tools: [ { name: "generate_blog_image", description: getToolDescription(), inputSchema: { type: "object" as const, properties: { prompt: { type: "string", description: "Description of the image to generate", minLength: 3, maxLength: 4000, }, format: { type: "string", description: `Platform preset: ${PRESET_KEYS.join(", ")}`, enum: PRESET_KEYS, default: "ghost-banner", }, quality: { type: "string", description: "Quality level", enum: ["standard", "high"], default: "standard", }, style: { type: "string", description: "Optional style hint (e.g., 'photorealistic', 'illustration')", maxLength: 200, }, title: { type: "string", description: "Optional blog post title for context", maxLength: 200, }, outputPath: { type: "string", description: "Optional path to save the image file", maxLength: 500, }, provider: { type: "string", description: "Image generation provider", enum: ["gemini"], default: "gemini", }, }, required: ["prompt"], }, }, - src/index.ts:121-179 (registration)Tool execution handler for generate_blog_image in the CallToolRequestSchema switch. Parses input via Zod schema, calls executeGenerateImage, and formats the response including image data (base64, mimeType) and detail text.
case "generate_blog_image": { // Parse and validate input const parseResult = GenerateImageInputSchema.safeParse(args); if (!parseResult.success) { return { content: [ { type: "text" as const, text: `Invalid input: ${parseResult.error.errors.map((e) => e.message).join(", ")}`, }, ], isError: true, }; } // Execute generation const result = await executeGenerateImage(parseResult.data); if (!result.success) { return { content: [ { type: "text" as const, text: `Error: ${result.error ?? result.message}`, }, ], isError: true, }; } // Return success with image const content: Array<{ type: "text" | "image"; text?: string; data?: string; mimeType?: string; }> = [ { type: "text" as const, text: result.message, }, ]; // Include image data if available if (result.image) { content.push({ type: "image" as const, data: result.image.base64Data, mimeType: result.image.mimeType, }); content.push({ type: "text" as const, text: `\nDetails:\n- Format: ${result.image.format}\n- Dimensions: ${result.image.dimensions.width}x${result.image.dimensions.height}\n- File size: ${result.image.fileSize}${result.image.savedTo ? `\n- Saved to: ${result.image.savedTo}` : ""}`, }); } return { content }; } - src/utils/image.ts:151-237 (helper)Helper function 'saveImage' called by the handler. Saves base64 image data to disk, handles directory creation, filename generation, and embeds PNG metadata (prompt, model, provider, format, style, title, timestamp) as tEXt chunks.
export async function saveImage( base64Data: string, outputPath: string, mimeType: string = "image/png", metadata?: ImageMetadata ): Promise<string> { // Resolve to absolute path let absolutePath = resolve(outputPath); // Check if outputPath is an existing directory or ends with a path separator const isDirectory = outputPath.endsWith("/") || outputPath.endsWith("\\") || (existsSync(absolutePath) && statSync(absolutePath).isDirectory()); if (isDirectory) { // Generate a filename and append to the directory path const filename = generateFilename("blog-image", mimeType); absolutePath = join(absolutePath, filename); } // Ensure the directory exists const dir = dirname(absolutePath); if (!existsSync(dir)) { await mkdir(dir, { recursive: true }); } // Add extension if missing let finalPath = absolutePath; if (!extname(finalPath)) { finalPath += getExtensionFromMimeType(mimeType); } // Decode image data const rawBuffer = Buffer.from(base64Data, "base64"); // Embed metadata if provided and image is PNG const finalBuffer = metadata && mimeType === "image/png" ? embedPngMetadata(rawBuffer, metadata) : rawBuffer; await writeFile(finalPath, finalBuffer); return finalPath; } /** * Calculate approximate file size from base64 data. */ export function estimateFileSize(base64Data: string): number { // Base64 is ~4/3 the size of the original binary return Math.floor((base64Data.length * 3) / 4); } /** * Format file size for display. */ export function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } /** * Get the default output directory for generated images. * Uses IMAGE_OUTPUT_DIR env var if set, otherwise falls back to cwd/generated-images. */ export function getDefaultOutputDir(): string { const envDir = process.env.IMAGE_OUTPUT_DIR; if (envDir) { return resolve(envDir); } return join(process.cwd(), "generated-images"); } /** * Generate a default output path for an image. * Creates a timestamped filename in the default output directory. */ export function generateDefaultOutputPath( mimeType: string = "image/png", prefix: string = "blog-image" ): string { const dir = getDefaultOutputDir(); const filename = generateFilename(prefix, mimeType); return join(dir, filename); }