@falldownthesystem/imagen-mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # imagen-mcp
2
2
 
3
- Minimal MCP server for OpenAI GPT Image 2.5. Two tools, two models.
3
+ Minimal MCP server for OpenAI GPT Image 2.5 and Krea 3. Generate and edit tools for each provider.
4
4
 
5
- | Alias | API model | Use |
5
+ | Provider | Tools | Models |
6
6
  |---|---|---|
7
- | `flare` (default) | `gpt-image-2.5-flare` | Fast, general purpose |
8
- | `sunburst` | `gpt-image-2.5-sunburst` | Slower, higher precision |
7
+ | OpenAI | `generate_image`, `edit_image` | `flare` (default, `gpt-image-2.5-flare`): fast, general purpose. `sunburst` (`gpt-image-2.5-sunburst`): slower, higher precision. |
8
+ | Krea | `krea_generate_image`, `krea_edit_image` | Krea 3: fast, instruction-following editor. Early-access beta. |
9
+
10
+ The server registers a provider's tools only when that provider's API key is set.
9
11
 
10
12
  ## Setup
11
13
 
@@ -22,7 +24,10 @@ npm install
22
24
  npm run build
23
25
  ```
24
26
 
25
- Set `OPENAI_API_KEY` in the environment, or put it in a `.env` file next to `package.json`.
27
+ Set one or both API keys in the environment, or put them in a `.env` file next to `package.json`:
28
+
29
+ - `OPENAI_API_KEY`: enables the OpenAI tools.
30
+ - `KREA_API_KEY`: enables the Krea tools. Create a token at [krea.ai/settings/api-tokens](https://www.krea.ai/settings/api-tokens). The Krea 3 API is in early access; only workspaces in the beta can call it. Request access at sales@krea.ai.
26
31
 
27
32
  Optional environment variables:
28
33
 
@@ -41,6 +46,7 @@ Claude Code (`.mcp.json`) or Claude Desktop, using the npm package:
41
46
  "args": ["-y", "@falldownthesystem/imagen-mcp"],
42
47
  "env": {
43
48
  "OPENAI_API_KEY": "sk-...",
49
+ "KREA_API_KEY": "...",
44
50
  "IMAGEN_OUTPUT_DIR": "C:/Users/me/Pictures/imagen"
45
51
  }
46
52
  }
@@ -53,10 +59,10 @@ Or with a local build, use `"command": "node"` and `"args": ["path/to/imagen-mcp
53
59
  Claude Code one-liner:
54
60
 
55
61
  ```sh
56
- claude mcp add imagen -e OPENAI_API_KEY=sk-... -- npx -y @falldownthesystem/imagen-mcp
62
+ claude mcp add imagen -e OPENAI_API_KEY=sk-... -e KREA_API_KEY=... -- npx -y @falldownthesystem/imagen-mcp
57
63
  ```
58
64
 
59
- ## Tools
65
+ ## OpenAI tools
60
66
 
61
67
  ### `generate_image`
62
68
 
@@ -74,7 +80,8 @@ Reference images to image. Required: `prompt`, `images`. The model regenerates t
74
80
  |---|---|---|---|
75
81
  | `images` | 1 to 16 file paths | required | `png`, `jpeg`, or `webp`. The first image is the main subject; extra images supply elements to combine, such as a logo or a style reference. Relative paths resolve against the server working directory. |
76
82
  | `mask` | file path | none | PNG with an alpha channel, same width and height as the first image. Fully transparent pixels mark the region the model may repaint; opaque pixels mark what to keep. Use for inpainting one area or filling an extended canvas. It is guidance, not a hard pixel lock. |
77
- | `input_fidelity` | `high`, `low` | `low` | How closely exact details from the references are preserved. `low` keeps content and layout, but faces, text, logos, and textures may shift. `high` keeps them faithful and steadies the area outside a mask. It costs more input tokens per reference image. Use `high` for real people, brands, product shots, or spot edits. |
83
+
84
+ There is no `input_fidelity` option. The GPT Image 2.5 models always process reference images at high fidelity, and the API rejects the parameter. Faces, logos, text, and textures from the references are preserved by default.
78
85
 
79
86
  ### Shared options
80
87
 
@@ -97,3 +104,48 @@ Only the options you set are sent to the API. The default column shows what appl
97
104
  | `return_image` | boolean | `false` | Also return each image inline as MCP image content, so the client shows it and the model can inspect it. Adds about 1.4 MB of context per 1024x1024 png. The file is saved to disk either way. |
98
105
 
99
106
  The result lists the saved file paths, the model, the size, and token usage.
107
+
108
+ ## Krea tools
109
+
110
+ Both tools call `POST /generate/image/krea/krea-3`, poll the job every 2 seconds, and download the result. A job that does not finish in 5 minutes, or a tool call that the client cancels, is cancelled on Krea. Failed and cancelled jobs are not billed.
111
+
112
+ ### `krea_generate_image`
113
+
114
+ Text to image. Required: `prompt`.
115
+
116
+ ### `krea_edit_image`
117
+
118
+ Reference images to image. Required: `prompt`, `images`. Write the prompt as an instruction: lead with the change, then add one sentence about what stays the same. With several references, name each one by what it shows, for example "Put the sneaker from the second image on the person in the first image."
119
+
120
+ | Option | Values | Default | Notes |
121
+ |---|---|---|---|
122
+ | `images` | 1 to 8 local paths or `https` URLs | required | Local files (`png`, `jpeg`, `webp`) are uploaded to Krea assets for the request and deleted after it. URLs are passed through. References over 4 MP are resized to 4 MP. With 5 or more references, each is resized to 1536 px on its long side, so send 4 or fewer for full detail. |
123
+ | `regional_edit` | boolean | `false` | `true` regenerates only the parts that must change and keeps the rest pixel-exact. Use it for local edits (recolor, add or remove an object, change text) and for chained edits on the latest output. Leave it off for global edits (restyle, relight, reframe, combine images into a new scene), which re-render the whole picture. |
124
+
125
+ There is no mask option. Krea 3 decides the edit region from the prompt.
126
+
127
+ ### Shared Krea options
128
+
129
+ Only the options you set are sent, except `thinking_mode` and `resolution`, which are always sent with their defaults.
130
+
131
+ | Option | Values | Default | Notes |
132
+ |---|---|---|---|
133
+ | `prompt` | text | required | |
134
+ | `thinking_mode` | `fast`, `high` | `fast` | How much Krea 3 plans before it generates. `high` uses a stronger reasoning engine for complex or multi-step edits and for combining several references. It is slower and costs $0.02 more. |
135
+ | `resolution` | `1K`, `2K` | `2K` | Output canvas size. 2K is the native resolution. |
136
+ | `aspect_ratio` | `1:1`, `4:5`, `3:4`, `2:3`, `9:16`, `5:4`, `4:3`, `3:2`, `16:9`, `21:9` | none | Omit for automatic framing. |
137
+ | `negative_prompt` | text, up to 1024 chars | none | What to avoid. |
138
+ | `seed` | integer | random | With `n` greater than 1, variation `i` uses `seed + i`. The same seed does not guarantee the same image, because the prompt expansion step varies between runs. |
139
+ | `n` | 1 to 4 | 1 | Each variation is a separate job, billed separately. If some jobs fail, the finished images are still saved and the failures are listed in `errors`. |
140
+ | `output_dir`, `file_name`, `return_image` | | | Same as the OpenAI tools. |
141
+
142
+ Pricing per image:
143
+
144
+ | Component | 1K output | 2K output |
145
+ |---|---|---|
146
+ | Base price | $0.03 | $0.06 |
147
+ | Each 1K reference image | +$0.02 | +$0.02 |
148
+ | Each 2K reference image | +$0.07 | +$0.07 |
149
+ | `high` thinking mode | +$0.02 | +$0.02 |
150
+
151
+ The result lists the saved file paths, the model, the resolution, the aspect ratio, the thinking mode, and the Krea job IDs.
package/dist/index.js CHANGED
@@ -1,11 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import fs from "node:fs/promises";
3
2
  import path from "node:path";
4
3
  import { McpServer } from "@modelcontextprotocol/server";
5
4
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
6
- import OpenAI, { toFile } from "openai";
7
- import * as z from "zod/v4";
8
- import { commonInput, resolveModel, resolveSize } from "./params.js";
5
+ import { registerKreaTools } from "./krea.js";
6
+ import { registerOpenAITools } from "./openai.js";
9
7
  // A .env next to the project is a convenience for local runs; MCP client configs normally pass env directly.
10
8
  try {
11
9
  process.loadEnvFile(path.join(import.meta.dirname, "..", ".env"));
@@ -13,114 +11,16 @@ try {
13
11
  catch {
14
12
  // No .env file present; rely on the process environment.
15
13
  }
16
- const client = new OpenAI();
17
- const server = new McpServer({ name: "imagen-mcp", version: "0.1.0" });
18
- const MIME = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
19
- const outputSchema = z.object({
20
- files: z.array(z.string()),
21
- model: z.string(),
22
- size: z.string(),
23
- usage: z
24
- .object({
25
- input_tokens: z.number(),
26
- output_tokens: z.number(),
27
- total_tokens: z.number(),
28
- input_tokens_details: z.object({ image_tokens: z.number(), text_tokens: z.number() }).optional(),
29
- output_tokens_details: z.object({ image_tokens: z.number(), text_tokens: z.number() }).optional(),
30
- })
31
- .optional(),
32
- });
33
- const annotations = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
34
- function slug(text) {
35
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "image";
14
+ const openaiKey = process.env.OPENAI_API_KEY;
15
+ const kreaKey = process.env.KREA_API_KEY;
16
+ if (!openaiKey && !kreaKey) {
17
+ console.error("imagen-mcp: set OPENAI_API_KEY, KREA_API_KEY, or both.");
18
+ process.exit(1);
36
19
  }
37
- function apiParams(input) {
38
- return {
39
- model: resolveModel(input.model),
40
- prompt: input.prompt,
41
- size: resolveSize(input),
42
- quality: input.quality,
43
- background: input.background,
44
- output_format: input.output_format,
45
- output_compression: input.output_compression,
46
- n: input.n,
47
- };
48
- }
49
- async function saveImages(result, input, model, size) {
50
- const images = result.data ?? [];
51
- if (images.length === 0)
52
- throw new Error("The API returned no images.");
53
- const ext = input.output_format ?? "png";
54
- const dir = path.resolve(input.output_dir ?? process.env.IMAGEN_OUTPUT_DIR ?? "output");
55
- await fs.mkdir(dir, { recursive: true });
56
- const base = input.file_name ?? `${new Date().toISOString().replace(/[:.]/g, "-")}-${slug(input.prompt)}`;
57
- const files = [];
58
- const content = [];
59
- for (const [i, image] of images.entries()) {
60
- if (!image.b64_json)
61
- throw new Error(`Image ${i + 1} has no b64_json payload.`);
62
- const file = path.join(dir, images.length > 1 ? `${base}-${i + 1}.${ext}` : `${base}.${ext}`);
63
- await fs.writeFile(file, Buffer.from(image.b64_json, "base64"));
64
- files.push(file);
65
- if (input.return_image)
66
- content.push({ type: "image", data: image.b64_json, mimeType: MIME[ext] });
67
- }
68
- const output = { files, model, size: size ?? "auto", usage: result.usage };
69
- content.unshift({ type: "text", text: JSON.stringify(output, null, 2) });
70
- return { content, structuredContent: output };
71
- }
72
- async function loadImage(file) {
73
- const abs = path.resolve(file);
74
- const ext = path.extname(abs).slice(1).toLowerCase();
75
- const type = MIME[ext === "jpg" ? "jpeg" : ext] ?? "application/octet-stream";
76
- return toFile(await fs.readFile(abs), path.basename(abs), { type });
77
- }
78
- server.registerTool("generate_image", {
79
- title: "Generate image",
80
- description: "Generate images from a text prompt with OpenAI GPT Image 2.5. Saves files to disk and returns paths and token usage.",
81
- inputSchema: z.object({
82
- ...commonInput,
83
- moderation: z
84
- .enum(["auto", "low"])
85
- .optional()
86
- .describe("OpenAI content filter. low refuses fewer borderline prompts. Default low."),
87
- }),
88
- outputSchema,
89
- annotations,
90
- }, async (input) => {
91
- const params = apiParams(input);
92
- const result = await client.images.generate({ ...params, moderation: input.moderation ?? "low" });
93
- return saveImages(result, input, params.model, params.size);
94
- });
95
- server.registerTool("edit_image", {
96
- title: "Edit image",
97
- description: "Edit, restyle, or combine 1-16 reference images with a text prompt using OpenAI GPT Image 2.5. The whole picture is regenerated; without a mask the prompt decides what changes. Saves files to disk and returns paths and token usage.",
98
- inputSchema: z.object({
99
- ...commonInput,
100
- images: z
101
- .array(z.string())
102
- .min(1)
103
- .max(16)
104
- .describe("Paths of 1-16 reference images (png/jpeg/webp). First = main subject; others = elements to combine. Relative to server cwd."),
105
- mask: z
106
- .string()
107
- .optional()
108
- .describe("Path of a PNG with alpha, same size as first image. Transparent pixels = region to repaint (guidance, not a hard lock). Default none."),
109
- input_fidelity: z
110
- .enum(["high", "low"])
111
- .optional()
112
- .describe("high keeps faces, text, logos and unmasked areas faithful; costs more input tokens. Default low."),
113
- }),
114
- outputSchema,
115
- annotations,
116
- }, async (input) => {
117
- const params = apiParams(input);
118
- const result = await client.images.edit({
119
- ...params,
120
- image: await Promise.all(input.images.map(loadImage)),
121
- mask: input.mask ? await loadImage(input.mask) : undefined,
122
- input_fidelity: input.input_fidelity,
123
- });
124
- return saveImages(result, input, params.model, params.size);
125
- });
20
+ const server = new McpServer({ name: "imagen-mcp", version: "0.2.0" });
21
+ // Tools are registered per configured provider so clients never see a tool that can only fail.
22
+ if (openaiKey)
23
+ registerOpenAITools(server);
24
+ if (kreaKey)
25
+ registerKreaTools(server, kreaKey);
126
26
  await server.connect(new StdioServerTransport());
package/dist/krea.js ADDED
@@ -0,0 +1,200 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { setTimeout as sleep } from "node:timers/promises";
4
+ import * as z from "zod/v4";
5
+ import { annotations, MIME, saveImages, outputInput } from "./output.js";
6
+ const API = "https://api.krea.ai";
7
+ const ENDPOINT = "/generate/image/krea/krea-3";
8
+ const POLL_INTERVAL_MS = 2000;
9
+ // Krea times out hosted jobs after 3 minutes; the extra margin covers queueing behind the concurrency limit.
10
+ const JOB_TIMEOUT_MS = 5 * 60_000;
11
+ const PENDING = new Set(["backlogged", "queued", "scheduled", "processing", "sampling", "intermediate-complete"]);
12
+ const ASPECT_RATIOS = ["1:1", "4:5", "3:4", "2:3", "9:16", "5:4", "4:3", "3:2", "16:9", "21:9"];
13
+ const kreaInput = {
14
+ prompt: z
15
+ .string()
16
+ .min(1)
17
+ .describe("Plain-language instruction. Lead with the change; end with one sentence on what to keep. Name multiple references by what they show."),
18
+ thinking_mode: z
19
+ .enum(["fast", "high"])
20
+ .optional()
21
+ .describe("Prompt planning before generation. high = stronger reasoning for complex or multi-reference edits, slower, +$0.02. Default fast."),
22
+ resolution: z.enum(["1K", "2K"]).optional().describe("Output canvas. 1K = $0.03, 2K = $0.06 base price. Default 2K."),
23
+ aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe("Default none (automatic framing)."),
24
+ negative_prompt: z.string().max(1024).optional().describe("What to avoid. Default none."),
25
+ seed: z
26
+ .number()
27
+ .int()
28
+ .optional()
29
+ .describe("Fixed seed; with n > 1 each variation uses seed + index. Prompt expansion still varies between runs. Default random."),
30
+ n: z
31
+ .number()
32
+ .int()
33
+ .min(1)
34
+ .max(4)
35
+ .optional()
36
+ .describe("Number of variations; each is a separate paid job. Files get -1, -2 suffixes. Default 1."),
37
+ ...outputInput,
38
+ };
39
+ const outputSchema = z.object({
40
+ files: z.array(z.string()),
41
+ model: z.string(),
42
+ resolution: z.string(),
43
+ aspect_ratio: z.string(),
44
+ thinking_mode: z.string(),
45
+ job_ids: z.array(z.string()),
46
+ errors: z.array(z.string()).optional(),
47
+ });
48
+ function sniffExt(data, url) {
49
+ if (data.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
50
+ return "png";
51
+ if (data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff)
52
+ return "jpeg";
53
+ if (data.toString("ascii", 0, 4) === "RIFF" && data.toString("ascii", 8, 12) === "WEBP")
54
+ return "webp";
55
+ // An unknown format is still a paid result, so keep it under whatever extension the URL carries.
56
+ return path.extname(new URL(url).pathname).slice(1).toLowerCase() || "img";
57
+ }
58
+ function jobError(job) {
59
+ const detail = job.error?.message ?? job.error?.code ?? job.result?.error;
60
+ return `Krea job ${job.job_id} ${job.status}${detail ? `: ${typeof detail === "string" ? detail : JSON.stringify(detail)}` : ""}`;
61
+ }
62
+ function createClient(apiKey) {
63
+ async function request(pathname, init = {}) {
64
+ const res = await fetch(`${API}${pathname}`, {
65
+ ...init,
66
+ headers: { Authorization: `Bearer ${apiKey}`, ...init.headers },
67
+ });
68
+ const text = await res.text();
69
+ if (!res.ok) {
70
+ let message = text;
71
+ try {
72
+ message = JSON.parse(text).error ?? text;
73
+ }
74
+ catch {
75
+ // Non-JSON error body; report it verbatim.
76
+ }
77
+ throw new Error(`Krea ${init.method ?? "GET"} ${pathname} failed (${res.status}): ${message}`);
78
+ }
79
+ return (text ? JSON.parse(text) : undefined);
80
+ }
81
+ async function upload(file, signal) {
82
+ const abs = path.resolve(file);
83
+ const ext = path.extname(abs).slice(1).toLowerCase();
84
+ const type = MIME[ext === "jpg" ? "jpeg" : ext] ?? "application/octet-stream";
85
+ const form = new FormData();
86
+ form.append("file", new Blob([await fs.readFile(abs)], { type }), path.basename(abs));
87
+ return request("/assets", { method: "POST", body: form, signal });
88
+ }
89
+ async function cancel(jobId) {
90
+ await request(`/jobs/${jobId}`, { method: "DELETE" }).catch(() => { });
91
+ }
92
+ async function run(body, signal) {
93
+ const created = await request(ENDPOINT, {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify(body),
97
+ signal,
98
+ });
99
+ const deadline = Date.now() + JOB_TIMEOUT_MS;
100
+ let job = created;
101
+ try {
102
+ while (PENDING.has(job.status)) {
103
+ if (Date.now() > deadline)
104
+ throw new Error(`Krea job ${job.job_id} still ${job.status} after ${JOB_TIMEOUT_MS / 1000}s.`);
105
+ await sleep(POLL_INTERVAL_MS, undefined, { signal });
106
+ job = await request(`/jobs/${job.job_id}`, { signal });
107
+ }
108
+ }
109
+ catch (error) {
110
+ // Failed and cancelled jobs are not billed, so stop work the caller will never receive.
111
+ await cancel(created.job_id);
112
+ throw error;
113
+ }
114
+ if (job.status !== "completed")
115
+ throw new Error(jobError(job));
116
+ const urls = job.result?.urls ?? [];
117
+ if (urls.length === 0)
118
+ throw new Error(`Krea job ${job.job_id} completed without output URLs.`);
119
+ const images = await Promise.all(urls.map(async (url) => {
120
+ const res = await fetch(url, { signal });
121
+ if (!res.ok)
122
+ throw new Error(`Download of ${url} failed (${res.status}).`);
123
+ const data = Buffer.from(await res.arrayBuffer());
124
+ return { data, ext: sniffExt(data, url) };
125
+ }));
126
+ return { jobId: job.job_id, images };
127
+ }
128
+ async function generate(input, signal) {
129
+ const uploaded = [];
130
+ try {
131
+ const imageUrls = input.images
132
+ ? await Promise.all(input.images.map(async (ref) => {
133
+ if (/^https?:\/\//i.test(ref))
134
+ return ref;
135
+ const asset = await upload(ref, signal);
136
+ uploaded.push(asset.id);
137
+ return asset.image_url;
138
+ }))
139
+ : undefined;
140
+ const resolution = input.resolution ?? "2K";
141
+ const thinkingMode = input.thinking_mode ?? "fast";
142
+ const runs = await Promise.allSettled(Array.from({ length: input.n ?? 1 }, (_, i) => run({
143
+ prompt: input.prompt,
144
+ thinking_mode: thinkingMode,
145
+ resolution,
146
+ aspect_ratio: input.aspect_ratio,
147
+ negative_prompt: input.negative_prompt,
148
+ seed: input.seed === undefined ? undefined : input.seed + i,
149
+ image_urls: imageUrls,
150
+ regional_edit: input.regional_edit,
151
+ }, signal)));
152
+ const done = runs.flatMap((r) => (r.status === "fulfilled" ? [r.value] : []));
153
+ const errors = runs.flatMap((r) => (r.status === "rejected" ? [String(r.reason instanceof Error ? r.reason.message : r.reason)] : []));
154
+ // Completed variations are already billed, so save them even when siblings fail.
155
+ if (done.length === 0)
156
+ throw new Error(errors.join("\n"));
157
+ return saveImages(done.flatMap((d) => d.images), input, {
158
+ model: "krea-3",
159
+ resolution,
160
+ aspect_ratio: input.aspect_ratio ?? "auto",
161
+ thinking_mode: thinkingMode,
162
+ job_ids: done.map((d) => d.jobId),
163
+ ...(errors.length > 0 && { errors }),
164
+ });
165
+ }
166
+ finally {
167
+ // Reference uploads exist only for this request; keep them out of the user's Krea asset library.
168
+ await Promise.allSettled(uploaded.map((id) => request(`/assets/${id}`, { method: "DELETE" })));
169
+ }
170
+ }
171
+ return { generate };
172
+ }
173
+ export function registerKreaTools(server, apiKey) {
174
+ const krea = createClient(apiKey);
175
+ server.registerTool("krea_generate_image", {
176
+ title: "Generate image (Krea 3)",
177
+ description: "Generate images from a text prompt with Krea 3 (early-access beta). Saves files to disk and returns paths and Krea job IDs.",
178
+ inputSchema: z.object(kreaInput),
179
+ outputSchema,
180
+ annotations,
181
+ }, (input, ctx) => krea.generate(input, ctx.mcpReq.signal));
182
+ server.registerTool("krea_edit_image", {
183
+ title: "Edit image (Krea 3)",
184
+ description: "Edit or combine 1-8 reference images with a plain-language instruction using Krea 3 (early-access beta). Without regional_edit the whole picture is re-rendered. Saves files to disk and returns paths and Krea job IDs.",
185
+ inputSchema: z.object({
186
+ ...kreaInput,
187
+ images: z
188
+ .array(z.string())
189
+ .min(1)
190
+ .max(8)
191
+ .describe("1-8 reference images: local paths (png/jpeg/webp, relative to server cwd) or https URLs. Each costs +$0.02 (1K) or +$0.07 (2K). Over 4 refs are downscaled to 1536 px."),
192
+ regional_edit: z
193
+ .boolean()
194
+ .optional()
195
+ .describe("true = regenerate only what must change and keep the rest pixel-exact; use for local edits and chained edits. Omit for restyle, relight, reframe, or compositing. Default false."),
196
+ }),
197
+ outputSchema,
198
+ annotations,
199
+ }, (input, ctx) => krea.generate(input, ctx.mcpReq.signal));
200
+ }
package/dist/openai.js ADDED
@@ -0,0 +1,93 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import OpenAI, { toFile } from "openai";
4
+ import * as z from "zod/v4";
5
+ import { annotations, MIME, saveImages } from "./output.js";
6
+ import { commonInput, resolveModel, resolveSize } from "./params.js";
7
+ const outputSchema = z.object({
8
+ files: z.array(z.string()),
9
+ model: z.string(),
10
+ size: z.string(),
11
+ usage: z
12
+ .object({
13
+ input_tokens: z.number(),
14
+ output_tokens: z.number(),
15
+ total_tokens: z.number(),
16
+ input_tokens_details: z.object({ image_tokens: z.number(), text_tokens: z.number() }).optional(),
17
+ output_tokens_details: z.object({ image_tokens: z.number(), text_tokens: z.number() }).optional(),
18
+ })
19
+ .optional(),
20
+ });
21
+ function apiParams(input) {
22
+ return {
23
+ model: resolveModel(input.model),
24
+ prompt: input.prompt,
25
+ size: resolveSize(input),
26
+ quality: input.quality,
27
+ background: input.background,
28
+ output_format: input.output_format,
29
+ output_compression: input.output_compression,
30
+ n: input.n,
31
+ };
32
+ }
33
+ function saveResult(result, input, model, size) {
34
+ const ext = input.output_format ?? "png";
35
+ const images = (result.data ?? []).map((image, i) => {
36
+ if (!image.b64_json)
37
+ throw new Error(`Image ${i + 1} has no b64_json payload.`);
38
+ return { data: Buffer.from(image.b64_json, "base64"), ext };
39
+ });
40
+ return saveImages(images, input, { model, size: size ?? "auto", usage: result.usage });
41
+ }
42
+ async function loadImage(file) {
43
+ const abs = path.resolve(file);
44
+ const ext = path.extname(abs).slice(1).toLowerCase();
45
+ const type = MIME[ext === "jpg" ? "jpeg" : ext] ?? "application/octet-stream";
46
+ return toFile(await fs.readFile(abs), path.basename(abs), { type });
47
+ }
48
+ export function registerOpenAITools(server) {
49
+ const client = new OpenAI();
50
+ server.registerTool("generate_image", {
51
+ title: "Generate image",
52
+ description: "Generate images from a text prompt with OpenAI GPT Image 2.5. Saves files to disk and returns paths and token usage.",
53
+ inputSchema: z.object({
54
+ ...commonInput,
55
+ moderation: z
56
+ .enum(["auto", "low"])
57
+ .optional()
58
+ .describe("OpenAI content filter. low refuses fewer borderline prompts. Default low."),
59
+ }),
60
+ outputSchema,
61
+ annotations,
62
+ }, async (input) => {
63
+ const params = apiParams(input);
64
+ const result = await client.images.generate({ ...params, moderation: input.moderation ?? "low" });
65
+ return saveResult(result, input, params.model, params.size);
66
+ });
67
+ server.registerTool("edit_image", {
68
+ title: "Edit image",
69
+ description: "Edit, restyle, or combine 1-16 reference images with a text prompt using OpenAI GPT Image 2.5. The whole picture is regenerated; without a mask the prompt decides what changes. Reference details are always kept at high fidelity (no input_fidelity option). Saves files to disk and returns paths and token usage.",
70
+ inputSchema: z.object({
71
+ ...commonInput,
72
+ images: z
73
+ .array(z.string())
74
+ .min(1)
75
+ .max(16)
76
+ .describe("Paths of 1-16 reference images (png/jpeg/webp). First = main subject; others = elements to combine. Relative to server cwd."),
77
+ mask: z
78
+ .string()
79
+ .optional()
80
+ .describe("Path of a PNG with alpha, same size as first image. Transparent pixels = region to repaint (guidance, not a hard lock). Default none."),
81
+ }),
82
+ outputSchema,
83
+ annotations,
84
+ }, async (input) => {
85
+ const params = apiParams(input);
86
+ const result = await client.images.edit({
87
+ ...params,
88
+ image: await Promise.all(input.images.map(loadImage)),
89
+ mask: input.mask ? await loadImage(input.mask) : undefined,
90
+ });
91
+ return saveResult(result, input, params.model, params.size);
92
+ });
93
+ }
package/dist/output.js ADDED
@@ -0,0 +1,36 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import * as z from "zod/v4";
4
+ export const MIME = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
5
+ export const outputInput = {
6
+ output_dir: z.string().optional().describe("Save directory. Default IMAGEN_OUTPUT_DIR env or ./output (relative to server cwd)."),
7
+ file_name: z.string().optional().describe("Base name without extension. Default timestamp + prompt slug."),
8
+ return_image: z
9
+ .boolean()
10
+ .optional()
11
+ .describe("Also return the image inline (base64, ~1.4 MB per 1024x1024 png) so it can be viewed. File is saved either way. Default false."),
12
+ };
13
+ export const annotations = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
14
+ function slug(text) {
15
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "image";
16
+ }
17
+ export async function saveImages(images, options, meta) {
18
+ if (images.length === 0)
19
+ throw new Error("The API returned no images.");
20
+ const dir = path.resolve(options.output_dir ?? process.env.IMAGEN_OUTPUT_DIR ?? "output");
21
+ await fs.mkdir(dir, { recursive: true });
22
+ const base = options.file_name ?? `${new Date().toISOString().replace(/[:.]/g, "-")}-${slug(options.prompt)}`;
23
+ const files = [];
24
+ const content = [];
25
+ for (const [i, image] of images.entries()) {
26
+ const file = path.join(dir, images.length > 1 ? `${base}-${i + 1}.${image.ext}` : `${base}.${image.ext}`);
27
+ await fs.writeFile(file, image.data);
28
+ files.push(file);
29
+ const mimeType = MIME[image.ext];
30
+ if (options.return_image && mimeType)
31
+ content.push({ type: "image", data: image.data.toString("base64"), mimeType });
32
+ }
33
+ const output = { files, ...meta };
34
+ content.unshift({ type: "text", text: JSON.stringify(output, null, 2) });
35
+ return { content, structuredContent: output };
36
+ }
package/dist/params.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod/v4";
2
+ import { outputInput } from "./output.js";
2
3
  export const MODELS = {
3
4
  flare: "gpt-image-2.5-flare",
4
5
  sunburst: "gpt-image-2.5-sunburst",
@@ -68,12 +69,7 @@ export const commonInput = {
68
69
  .max(10)
69
70
  .optional()
70
71
  .describe("Number of variations to generate; each costs output tokens. Files get -1, -2 suffixes. Default 1."),
71
- output_dir: z.string().optional().describe("Save directory. Default IMAGEN_OUTPUT_DIR env or ./output (relative to server cwd)."),
72
- file_name: z.string().optional().describe("Base name without extension. Default timestamp + prompt slug."),
73
- return_image: z
74
- .boolean()
75
- .optional()
76
- .describe("Also return the image inline (base64, ~1.4 MB per 1024x1024 png) so it can be viewed. File is saved either way. Default false."),
72
+ ...outputInput,
77
73
  };
78
74
  export function resolveModel(alias) {
79
75
  const fallback = process.env.IMAGEN_DEFAULT_MODEL;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@falldownthesystem/imagen-mcp",
3
- "version": "0.1.0",
4
- "description": "Minimal MCP server for OpenAI GPT Image 2.5 (flare and sunburst): generate and edit images",
3
+ "version": "0.2.0",
4
+ "description": "Minimal MCP server for OpenAI GPT Image 2.5 (flare and sunburst) and Krea 3: generate and edit images",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "imagen-mcp": "dist/index.js"
@@ -23,6 +23,7 @@
23
23
  "model-context-protocol",
24
24
  "openai",
25
25
  "gpt-image",
26
+ "krea",
26
27
  "image-generation"
27
28
  ],
28
29
  "author": "FallDownTheSystem",
@@ -36,12 +37,12 @@
36
37
  "access": "public"
37
38
  },
38
39
  "dependencies": {
39
- "@modelcontextprotocol/server": "^2.0.0",
40
- "openai": "^7.13.0",
41
- "zod": "^4.5.4"
40
+ "@modelcontextprotocol/server": "^2.3.1",
41
+ "openai": "^7.34.0",
42
+ "zod": "^4.6.7"
42
43
  },
43
44
  "devDependencies": {
44
- "@types/node": "^26.5.0",
45
+ "@types/node": "^26.6.5",
45
46
  "typescript": "^7.0.2"
46
47
  }
47
48
  }