index.js•7.39 kB
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import dotenv from "dotenv";
import { readFile, writeFile } from "fs/promises";
import { resolve } from "path";
dotenv.config();
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
const BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent";
const ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
if (!GEMINI_API_KEY) {
console.error("Error: GEMINI_API_KEY environment variable is required");
console.error("Get your API key at: https://aistudio.google.com/apikey");
process.exit(1);
}
class GeminiImageServer {
constructor() {
this.server = new Server(
{
name: "gemini-image-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
this.server.onerror = (error) => console.error("[MCP Error]", error);
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "generate_image",
description:
"Generate or edit images using Gemini 2.5 Flash Image (Nano Banana). " +
"Supports text-to-image generation, image editing with natural language prompts, " +
"and multi-image composition. All generated images include a SynthID watermark.",
inputSchema: {
type: "object",
properties: {
prompt: {
type: "string",
description: "Text prompt describing the image to generate or edits to make",
},
input_images: {
type: "array",
items: {
type: "string",
},
description: "Optional array of file paths to input images for editing or composition",
},
aspect_ratio: {
type: "string",
enum: ASPECT_RATIOS,
description: "Output aspect ratio. Options: " + ASPECT_RATIOS.join(", "),
default: "1:1",
},
output_path: {
type: "string",
description: "Path where the generated image will be saved (must end in .png)",
default: "output.png",
},
image_only: {
type: "boolean",
description: "If true, requests image-only output without text response",
default: false,
},
},
required: ["prompt", "output_path"],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== "generate_image") {
throw new Error(`Unknown tool: ${request.params.name}`);
}
const {
prompt,
input_images = [],
aspect_ratio = "1:1",
output_path = "output.png",
image_only = false,
} = request.params.arguments;
if (!prompt) {
throw new Error("prompt is required");
}
if (!output_path.endsWith(".png")) {
throw new Error("output_path must end with .png");
}
if (!ASPECT_RATIOS.includes(aspect_ratio)) {
throw new Error(`Invalid aspect_ratio. Must be one of: ${ASPECT_RATIOS.join(", ")}`);
}
try {
// Build request parts
const parts = [{ text: prompt }];
// Add input images if provided
for (const imagePath of input_images) {
const imageData = await this.encodeImage(imagePath);
parts.push(imageData);
}
// Build API request payload
const payload = {
contents: [{ parts }],
generationConfig: {
responseModalities: image_only ? ["Image"] : ["Text", "Image"],
imageConfig: {
aspectRatio: aspect_ratio,
},
},
};
// Make API request
const url = `${BASE_URL}?key=${GEMINI_API_KEY}`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText}\n${errorText}`);
}
const result = await response.json();
// Extract and save image
let textResponse = null;
let imageSaved = false;
if (result.candidates) {
for (const candidate of result.candidates) {
if (candidate.content) {
for (const part of candidate.content.parts || []) {
if (part.inlineData) {
await this.saveImage(part.inlineData, output_path);
imageSaved = true;
} else if (part.text) {
textResponse = part.text;
}
}
}
}
}
if (!imageSaved) {
throw new Error("No image was generated in the response");
}
// Return success response
const responseText = [
`✓ Image generated successfully!`,
` Saved to: ${output_path}`,
` Aspect ratio: ${aspect_ratio}`,
textResponse ? ` AI response: ${textResponse}` : null,
]
.filter(Boolean)
.join("\n");
return {
content: [
{
type: "text",
text: responseText,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error generating image: ${error.message}`,
},
],
isError: true,
};
}
});
}
async encodeImage(imagePath) {
const absolutePath = resolve(imagePath);
const imageBuffer = await readFile(absolutePath);
const base64Data = imageBuffer.toString("base64");
// Determine MIME type from extension
const mimeTypes = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".webp": "image/webp",
".gif": "image/gif",
};
const ext = imagePath.toLowerCase().match(/\.\w+$/)?.[0];
const mimeType = mimeTypes[ext] || "image/jpeg";
return {
inlineData: {
mimeType,
data: base64Data,
},
};
}
async saveImage(inlineData, outputPath) {
const absolutePath = resolve(outputPath);
const imageBuffer = Buffer.from(inlineData.data, "base64");
await writeFile(absolutePath, imageBuffer);
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Gemini Image MCP server running on stdio");
}
}
const server = new GeminiImageServer();
server.run().catch(console.error);