imagen-switch-mcp 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/index.js +723 -0
  4. package/package.json +47 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 imagen-switch-mcp contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,331 @@
1
+ # imagen-switch-mcp
2
+
3
+ `imagen-switch-mcp` 是一个 TypeScript 实现的 MCP server,用环境变量把任意 MCP Agent 连接到图像生成/编辑端点。
4
+
5
+ 它内置 OpenAI Images、OpenAI 兼容网关、Gemini 图像接口,并提供 `custom` 数据驱动适配器,用于接入长尾 Provider。
6
+
7
+ ## 功能
8
+
9
+ - MCP 工具:`generate_image` 与 `edit_image`
10
+ - Provider:`openai`、`gemini`、`custom`
11
+ - 输入图:本地路径、data URL、裸 base64、HTTP(S) URL
12
+ - 输出:默认保存到本地并返回绝对路径,可选内联返回 MCP image 内容块
13
+ - HTTP:统一认证注入、超时、429/5xx/网络错误重试、友好错误信息与 API key 脱敏
14
+ - 分发:发布到 npm 后可通过 `npx -y imagen-switch-mcp` 直接启动
15
+
16
+ ## 快速开始
17
+
18
+ ### 作为 npm 包使用
19
+
20
+ 发布后,在 Agent 的 MCP 配置里使用:
21
+
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "imagen": {
26
+ "command": "npx",
27
+ "args": ["-y", "imagen-switch-mcp"],
28
+ "env": {
29
+ "IMAGEN_FORMAT": "openai",
30
+ "IMAGEN_BASE_URL": "https://api.openai.com/v1",
31
+ "IMAGEN_API_KEY": "sk-...",
32
+ "IMAGEN_MODEL": "gpt-image-2",
33
+ "IMAGEN_OUTPUT_DIR": "D:/generated/imagen"
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### 本地开发版本使用
41
+
42
+ 还没发布到 npm 时,先构建:
43
+
44
+ ```powershell
45
+ cd D:\Repo\projects\imagen-switch
46
+ npm install
47
+ npm run build
48
+ ```
49
+
50
+ 然后让 Agent 指向本地构建产物:
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "imagen": {
56
+ "command": "node",
57
+ "args": ["D:/Repo/projects/imagen-switch/dist/index.js"],
58
+ "env": {
59
+ "IMAGEN_FORMAT": "openai",
60
+ "IMAGEN_BASE_URL": "https://api.openai.com/v1",
61
+ "IMAGEN_API_KEY": "sk-...",
62
+ "IMAGEN_MODEL": "gpt-image-2",
63
+ "IMAGEN_OUTPUT_DIR": "D:/Repo/projects/imagen-switch/.imagen"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ 配置后重启 Agent。工具列表中应出现:
71
+
72
+ - `generate_image`
73
+ - `edit_image`
74
+
75
+ ## 工具
76
+
77
+ ### `generate_image`
78
+
79
+ ```text
80
+ generate_image(prompt, model?, size?, n?, output_path?, ...provider 参数)
81
+ ```
82
+
83
+ OpenAI 格式常用参数:
84
+
85
+ ```text
86
+ generate_image(
87
+ prompt="一只透明背景的极简柴犬贴纸",
88
+ background="transparent",
89
+ output_format="png"
90
+ )
91
+ ```
92
+
93
+ ### `edit_image`
94
+
95
+ ```text
96
+ edit_image(prompt, images[], mask?, model?, size?, n?, output_path?, ...provider 参数)
97
+ ```
98
+
99
+ `images` 支持:
100
+
101
+ - 本地文件路径
102
+ - `data:image/png;base64,...`
103
+ - 裸 base64
104
+ - `https://...`
105
+
106
+ ## 环境变量
107
+
108
+ | 变量 | 说明 | 默认 |
109
+ | --- | --- | --- |
110
+ | `IMAGEN_FORMAT` | `openai`、`gemini`、`custom` | `openai` |
111
+ | `IMAGEN_BASE_URL` | Provider API 基址 | openai/gemini 有内置默认 |
112
+ | `IMAGEN_API_KEY` | Provider API key | 无 |
113
+ | `IMAGEN_MODEL` | 默认模型 | 无 |
114
+ | `IMAGEN_OUTPUT_DIR` | 图像保存目录 | 系统临时目录下 `imagen-switch/` |
115
+ | `IMAGEN_RETURN_INLINE` | 是否同时返回 MCP image 内容块 | `false` |
116
+ | `IMAGEN_TIMEOUT_MS` | 单次请求超时 | `120000` |
117
+ | `IMAGEN_MAX_RETRIES` | 429/5xx/网络错误重试次数 | `2` |
118
+ | `IMAGEN_EXTRA_BODY` | 额外 JSON body,JSON 对象字符串 | `{}` |
119
+ | `IMAGEN_EXTRA_HEADERS` | 额外 headers,JSON 对象字符串 | `{}` |
120
+ | `IMAGEN_EXTRA_QUERY` | 额外 query,JSON 对象字符串 | `{}` |
121
+
122
+ `custom` 专用变量:
123
+
124
+ | 变量 | 说明 |
125
+ | --- | --- |
126
+ | `IMAGEN_CUSTOM_GENERATE_PATH` | 生成接口路径,如 `/v1/text2image` |
127
+ | `IMAGEN_CUSTOM_EDIT_PATH` | 编辑接口路径;配置后才注册 `edit_image` |
128
+ | `IMAGEN_CUSTOM_ENCODING` | `json` 或 `multipart` |
129
+ | `IMAGEN_CUSTOM_BODY_TEMPLATE` | 请求体模板,支持 `{{prompt}}`、`{{model}}`、`{{size}}`、`{{n}}`、`{{image}}` |
130
+ | `IMAGEN_CUSTOM_RESPONSE_IMAGES_PATH` | 响应图像路径,如 `output.images[*]` |
131
+ | `IMAGEN_CUSTOM_RESPONSE_IMAGE_KIND` | `b64`、`url`、`dataurl` |
132
+
133
+ 完整设计背景见 [设计文档](docs/superpowers/specs/2026-06-23-imagen-switch-mcp-design.md)。
134
+
135
+ ## Provider 配方
136
+
137
+ ### OpenAI
138
+
139
+ ```json
140
+ {
141
+ "IMAGEN_FORMAT": "openai",
142
+ "IMAGEN_BASE_URL": "https://api.openai.com/v1",
143
+ "IMAGEN_API_KEY": "sk-...",
144
+ "IMAGEN_MODEL": "gpt-image-2"
145
+ }
146
+ ```
147
+
148
+ ### OpenAI 兼容网关
149
+
150
+ ```json
151
+ {
152
+ "IMAGEN_FORMAT": "openai",
153
+ "IMAGEN_BASE_URL": "https://your-gateway.example/v1",
154
+ "IMAGEN_API_KEY": "...",
155
+ "IMAGEN_MODEL": "flux.1-schnell"
156
+ }
157
+ ```
158
+
159
+ ### Gemini
160
+
161
+ ```json
162
+ {
163
+ "IMAGEN_FORMAT": "gemini",
164
+ "IMAGEN_BASE_URL": "https://generativelanguage.googleapis.com/v1beta",
165
+ "IMAGEN_API_KEY": "...",
166
+ "IMAGEN_MODEL": "gemini-2.5-flash-image"
167
+ }
168
+ ```
169
+
170
+ ### custom
171
+
172
+ ```json
173
+ {
174
+ "IMAGEN_FORMAT": "custom",
175
+ "IMAGEN_BASE_URL": "https://api.example.com",
176
+ "IMAGEN_API_KEY": "...",
177
+ "IMAGEN_CUSTOM_GENERATE_PATH": "/v1/text2image",
178
+ "IMAGEN_CUSTOM_ENCODING": "json",
179
+ "IMAGEN_CUSTOM_BODY_TEMPLATE": "{\"model\":\"{{model}}\",\"prompt\":\"{{prompt}}\",\"num\":{{n}}}",
180
+ "IMAGEN_CUSTOM_RESPONSE_IMAGES_PATH": "output.images[*]",
181
+ "IMAGEN_CUSTOM_RESPONSE_IMAGE_KIND": "url"
182
+ }
183
+ ```
184
+
185
+ ## 真实端点人工测试
186
+
187
+ 真实端点测试会消耗 Provider 额度。不要把 API key 写进仓库文件。
188
+
189
+ 先构建:
190
+
191
+ ```powershell
192
+ npm install
193
+ npm run build
194
+ ```
195
+
196
+ 设置环境变量,以 OpenAI 为例:
197
+
198
+ ```powershell
199
+ $env:IMAGEN_FORMAT="openai"
200
+ $env:IMAGEN_BASE_URL="https://api.openai.com/v1"
201
+ $env:IMAGEN_API_KEY="你的真实 API Key"
202
+ $env:IMAGEN_MODEL="gpt-image-2"
203
+ $env:IMAGEN_OUTPUT_DIR="D:\Repo\projects\imagen-switch\.imagen-real"
204
+ ```
205
+
206
+ 可以使用仓库根目录的 `tmp-real-test.mjs`,也可以临时创建同名文件。通用测试脚本如下:
207
+
208
+ ```js
209
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
210
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
211
+
212
+ const transport = new StdioClientTransport({
213
+ command: "node",
214
+ args: ["dist/index.js"],
215
+ env: process.env,
216
+ });
217
+
218
+ const client = new Client({ name: "real-test", version: "0.0.0" });
219
+ await client.connect(transport);
220
+
221
+ const tools = await client.listTools();
222
+ console.log("tools:", tools.tools.map((tool) => tool.name));
223
+
224
+ const result = await client.callTool(
225
+ {
226
+ name: "generate_image",
227
+ arguments: {
228
+ prompt: "一只透明背景的极简柴犬贴纸",
229
+ background: "transparent",
230
+ output_format: "png",
231
+ },
232
+ },
233
+ undefined,
234
+ { timeout: 300_000 },
235
+ );
236
+
237
+ console.dir(result, { depth: null });
238
+ await client.close();
239
+ ```
240
+
241
+ 运行:
242
+
243
+ ```powershell
244
+ node .\tmp-real-test.mjs
245
+ ```
246
+
247
+ 成功时返回文本中会包含保存路径,例如:
248
+
249
+ ```text
250
+ 已生成 1 张图像(provider=openai, model=gpt-image-2):
251
+ - D:\Repo\projects\imagen-switch\.imagen-real\imagen-0000000000000-0.png
252
+ ```
253
+
254
+ 测试结束后清理环境变量:
255
+
256
+ ```powershell
257
+ Remove-Item Env:\IMAGEN_FORMAT,Env:\IMAGEN_BASE_URL,Env:\IMAGEN_API_KEY,Env:\IMAGEN_MODEL,Env:\IMAGEN_OUTPUT_DIR -ErrorAction SilentlyContinue
258
+ ```
259
+
260
+ ## 开发
261
+
262
+ ```powershell
263
+ npm install
264
+ npm test
265
+ npm run typecheck
266
+ npm run build
267
+ ```
268
+
269
+ 常用脚本:
270
+
271
+ | 命令 | 说明 |
272
+ | --- | --- |
273
+ | `npm test` | 运行 Vitest |
274
+ | `npm run typecheck` | TypeScript 类型检查 |
275
+ | `npm run build` | 通过 tsup 构建 `dist/index.js` |
276
+ | `npm run ci` | 测试、类型检查、构建 |
277
+ | `npm run publish:dry-run` | 本地检查 npm 发布包内容 |
278
+
279
+ 目录结构:
280
+
281
+ ```text
282
+ src/
283
+ adapters/ Provider 适配器与 registry
284
+ config.ts 环境变量解析与校验
285
+ errors.ts 友好错误与 API key 脱敏
286
+ http.ts fetch、认证、超时、重试、下载
287
+ input.ts 输入图解析
288
+ output.ts 图像 materialize、落盘、工具返回
289
+ server.ts MCP server 工具注册与流水线
290
+ index.ts stdio 入口
291
+ test/
292
+ ```
293
+
294
+ ## 发布到 npm
295
+
296
+ 发布前确认:
297
+
298
+ ```powershell
299
+ npm run ci
300
+ npm run publish:dry-run
301
+ ```
302
+
303
+ 手动发布:
304
+
305
+ ```powershell
306
+ npm publish --provenance --access public
307
+ ```
308
+
309
+ 自动发布使用 `.github/workflows/npm-publish.yml`,只监听 `main` 分支。
310
+
311
+ 当前 workflow 使用 npm automation token,适合包的首次发布,也能避开 publish 2FA 在 CI 中要求一次性验证码的问题:
312
+
313
+ 1. 在 npm 创建 `Automation` 类型的 access token。
314
+ 2. 在 GitHub 仓库设置 `Settings -> Secrets and variables -> Actions` 中新增 secret:`NPM_AUTOMATION_TOKEN`。
315
+ 3. 不要使用普通 classic/granular publish token;如果账号开启了 publish 2FA,这类 token 会在 CI 中触发 `EOTP`,因为 Action 无法输入一次性验证码。
316
+
317
+ 发布成功后,如果想改成 npm Trusted Publishing,可以在 npm package 的发布设置里添加 GitHub Actions trusted publisher,并相应调整 workflow 移除 token 校验。
318
+
319
+ 合并到 `main` 后,Action 会运行 `npm run ci`。如果 `package.json` 中的版本还没有发布过,Action 会执行 `npm publish --provenance --access public`;如果版本已存在于 npm,Action 会跳过发布,避免主分支文档更新导致失败。
320
+
321
+ 发布新版本时,请先更新 `package.json` 里的 `version`,再合并到主分支。
322
+
323
+ ## 安全
324
+
325
+ - 不要把 `IMAGEN_API_KEY` 写入仓库。
326
+ - `.env`、`.env.*`、`.imagen/`、`.imagen-real/` 已被忽略。
327
+ - 错误信息会对已配置的 API key 做脱敏处理。
328
+
329
+ ## License
330
+
331
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,723 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { pathToFileURL } from "url";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/errors.ts
8
+ var ConfigError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "ConfigError";
12
+ }
13
+ };
14
+ var ProviderError = class extends Error {
15
+ status;
16
+ constructor(message, status) {
17
+ super(message);
18
+ this.name = "ProviderError";
19
+ this.status = status;
20
+ }
21
+ };
22
+ function redactKey(text, apiKey) {
23
+ if (!apiKey) return text;
24
+ return text.split(apiKey).join("***");
25
+ }
26
+ function friendlyHttpError(status, providerBody) {
27
+ const body = providerBody.slice(0, 800);
28
+ if (status === 401 || status === 403) {
29
+ return `\u8BA4\u8BC1\u5931\u8D25 (HTTP ${status})\uFF1A\u8BF7\u68C0\u67E5 IMAGEN_API_KEY / IMAGEN_AUTH_*\u3002Provider \u8FD4\u56DE\uFF1A${body}`;
30
+ }
31
+ if (status === 429) {
32
+ return `\u89E6\u53D1\u9650\u6D41 (HTTP 429)\uFF1A\u8BF7\u7A0D\u540E\u91CD\u8BD5\u6216\u964D\u4F4E\u9891\u7387\u3002Provider \u8FD4\u56DE\uFF1A${body}`;
33
+ }
34
+ if (status === 400) {
35
+ return `\u8BF7\u6C42\u53C2\u6570\u88AB\u62D2 (HTTP 400)\uFF1A${body}`;
36
+ }
37
+ return `Provider \u8FD4\u56DE\u9519\u8BEF (HTTP ${status})\uFF1A${body}`;
38
+ }
39
+
40
+ // src/config.ts
41
+ function parseJsonEnv(name, value) {
42
+ if (!value) return {};
43
+ try {
44
+ const parsed = JSON.parse(value);
45
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
46
+ throw new Error("not an object");
47
+ }
48
+ return parsed;
49
+ } catch (e) {
50
+ throw new ConfigError(`\u73AF\u5883\u53D8\u91CF ${name} \u4E0D\u662F\u5408\u6CD5\u7684 JSON \u5BF9\u8C61\uFF1A${e.message}`);
51
+ }
52
+ }
53
+ function parseIntEnv(name, value, fallback) {
54
+ if (value === void 0 || value === "") return fallback;
55
+ const n = Number(value);
56
+ if (!Number.isFinite(n)) {
57
+ throw new ConfigError(`\u73AF\u5883\u53D8\u91CF ${name} \u5FC5\u987B\u662F\u6570\u5B57\uFF0C\u6536\u5230\uFF1A${value}`);
58
+ }
59
+ return n;
60
+ }
61
+ function optionalCustomImageKind(value) {
62
+ if (value === void 0 || value === "") return void 0;
63
+ if (value === "b64" || value === "url" || value === "dataurl") return value;
64
+ throw new ConfigError(`IMAGEN_CUSTOM_RESPONSE_IMAGE_KIND \u5FC5\u987B\u662F b64|url|dataurl\uFF0C\u6536\u5230\uFF1A${value}`);
65
+ }
66
+ function loadRawConfig(env) {
67
+ const style = env.IMAGEN_AUTH_STYLE;
68
+ if (style && !["bearer", "header", "query", "none"].includes(style)) {
69
+ throw new ConfigError(`IMAGEN_AUTH_STYLE \u5FC5\u987B\u662F bearer|header|query|none\uFF0C\u6536\u5230\uFF1A${style}`);
70
+ }
71
+ const encoding = env.IMAGEN_CUSTOM_ENCODING ?? "json";
72
+ if (!["json", "multipart"].includes(encoding)) {
73
+ throw new ConfigError(`IMAGEN_CUSTOM_ENCODING \u5FC5\u987B\u662F json|multipart\uFF0C\u6536\u5230\uFF1A${encoding}`);
74
+ }
75
+ return {
76
+ format: env.IMAGEN_FORMAT ?? "openai",
77
+ baseUrl: env.IMAGEN_BASE_URL,
78
+ apiKey: env.IMAGEN_API_KEY,
79
+ model: env.IMAGEN_MODEL,
80
+ authStyle: style,
81
+ authHeaderName: env.IMAGEN_AUTH_HEADER_NAME,
82
+ authQueryName: env.IMAGEN_AUTH_QUERY_NAME,
83
+ outputDir: env.IMAGEN_OUTPUT_DIR,
84
+ returnInline: env.IMAGEN_RETURN_INLINE === "true",
85
+ timeoutMs: parseIntEnv("IMAGEN_TIMEOUT_MS", env.IMAGEN_TIMEOUT_MS, 12e4),
86
+ maxRetries: parseIntEnv("IMAGEN_MAX_RETRIES", env.IMAGEN_MAX_RETRIES, 2),
87
+ extraBody: parseJsonEnv("IMAGEN_EXTRA_BODY", env.IMAGEN_EXTRA_BODY),
88
+ extraHeaders: parseJsonEnv("IMAGEN_EXTRA_HEADERS", env.IMAGEN_EXTRA_HEADERS),
89
+ extraQuery: parseJsonEnv("IMAGEN_EXTRA_QUERY", env.IMAGEN_EXTRA_QUERY),
90
+ custom: {
91
+ generatePath: env.IMAGEN_CUSTOM_GENERATE_PATH,
92
+ editPath: env.IMAGEN_CUSTOM_EDIT_PATH,
93
+ encoding,
94
+ bodyTemplate: env.IMAGEN_CUSTOM_BODY_TEMPLATE,
95
+ responseImagesPath: env.IMAGEN_CUSTOM_RESPONSE_IMAGES_PATH,
96
+ responseImageKind: optionalCustomImageKind(env.IMAGEN_CUSTOM_RESPONSE_IMAGE_KIND)
97
+ }
98
+ };
99
+ }
100
+ function resolveBaseUrl(raw, defaultBaseUrl) {
101
+ const url = raw.baseUrl ?? defaultBaseUrl;
102
+ if (!url) {
103
+ throw new ConfigError(`\u7F3A\u5C11 IMAGEN_BASE_URL\uFF0C\u4E14 format "${raw.format}" \u65E0\u5185\u7F6E\u9ED8\u8BA4\u503C`);
104
+ }
105
+ return url.replace(/\/+$/, "");
106
+ }
107
+ function resolveAuth(raw, defaults) {
108
+ return {
109
+ style: raw.authStyle ?? defaults.style,
110
+ headerName: raw.authHeaderName ?? defaults.headerName,
111
+ queryName: raw.authQueryName ?? defaults.queryName,
112
+ apiKey: raw.apiKey
113
+ };
114
+ }
115
+
116
+ // src/server.ts
117
+ import { tmpdir } from "os";
118
+ import { join as join2 } from "path";
119
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
120
+ import { z as z2 } from "zod";
121
+
122
+ // src/adapters/openai.ts
123
+ import { z } from "zod";
124
+ var OPENAI_META = {
125
+ format: "openai",
126
+ defaultBaseUrl: "https://api.openai.com/v1",
127
+ defaultAuth: { style: "bearer" },
128
+ supportsEdit: true,
129
+ extraParams: {
130
+ quality: z.string().optional().describe("standard/high (gpt-image-2); low/medium/high/auto (gpt-image-1); standard/hd (dall-e-3)"),
131
+ background: z.string().optional().describe("auto | transparent | opaque"),
132
+ thinking: z.string().optional().describe("off | low | medium | high"),
133
+ seed: z.number().int().optional().describe("int32"),
134
+ output_format: z.string().optional().describe("png | jpeg | webp"),
135
+ output_compression: z.number().int().optional().describe("0-100"),
136
+ moderation: z.string().optional().describe("auto | low"),
137
+ user: z.string().optional().describe("abuse detection identifier")
138
+ }
139
+ };
140
+ function parseResponse(raw) {
141
+ const data = raw?.data;
142
+ if (!Array.isArray(data)) return [];
143
+ const out = [];
144
+ for (const item of data) {
145
+ const image = item;
146
+ if (typeof image.b64_json === "string") {
147
+ out.push({ kind: "b64", data: image.b64_json });
148
+ } else if (typeof image.url === "string") {
149
+ out.push({ kind: "url", data: image.url });
150
+ }
151
+ }
152
+ return out;
153
+ }
154
+ function appendFormValue(fd, key, value) {
155
+ if (value === void 0 || value === null) return;
156
+ fd.set(key, typeof value === "object" ? JSON.stringify(value) : String(value));
157
+ }
158
+ function imageBlob(image) {
159
+ const copy = new Uint8Array(image.bytes.length);
160
+ copy.set(image.bytes);
161
+ return new Blob([copy.buffer], { type: image.mime });
162
+ }
163
+ function createOpenAiAdapter(baseUrl) {
164
+ return {
165
+ ...OPENAI_META,
166
+ buildGenerate(req) {
167
+ const body = {
168
+ ...req.params,
169
+ model: req.model,
170
+ prompt: req.prompt,
171
+ n: req.n
172
+ };
173
+ if (req.size) body.size = req.size;
174
+ return { method: "POST", url: `${baseUrl}/images/generations`, headers: {}, body };
175
+ },
176
+ buildEdit(req, images, mask) {
177
+ const fd = new FormData();
178
+ fd.set("prompt", req.prompt);
179
+ fd.set("model", req.model);
180
+ fd.set("n", String(req.n));
181
+ if (req.size) fd.set("size", req.size);
182
+ for (const [key, value] of Object.entries(req.params)) {
183
+ appendFormValue(fd, key, value);
184
+ }
185
+ const field = images.length > 1 ? "image[]" : "image";
186
+ for (const img of images) {
187
+ fd.append(field, imageBlob(img), "image.png");
188
+ }
189
+ if (mask) fd.set("mask", imageBlob(mask), "mask.png");
190
+ return { method: "POST", url: `${baseUrl}/images/edits`, headers: {}, body: fd };
191
+ },
192
+ parseResponse
193
+ };
194
+ }
195
+
196
+ // src/adapters/gemini.ts
197
+ var GEMINI_META = {
198
+ format: "gemini",
199
+ defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
200
+ defaultAuth: { style: "header", headerName: "x-goog-api-key" },
201
+ supportsEdit: true,
202
+ extraParams: {}
203
+ };
204
+ function bytesToB64(bytes) {
205
+ return Buffer.from(bytes).toString("base64");
206
+ }
207
+ function baseGenerationConfig(req) {
208
+ const existing = req.params.generationConfig ?? {};
209
+ const generationConfig = {
210
+ responseModalities: ["TEXT", "IMAGE"],
211
+ ...existing
212
+ };
213
+ if (req.size) {
214
+ generationConfig.imageConfig = {
215
+ ...generationConfig.imageConfig ?? {},
216
+ aspectRatio: req.size
217
+ };
218
+ }
219
+ return generationConfig;
220
+ }
221
+ function parseResponse2(raw) {
222
+ const parts = raw?.candidates?.[0]?.content?.parts ?? [];
223
+ const out = [];
224
+ for (const part of parts) {
225
+ const inline = part.inline_data ?? part.inlineData;
226
+ if (inline?.data) {
227
+ out.push({
228
+ kind: "b64",
229
+ data: inline.data,
230
+ mime: inline.mime_type ?? inline.mimeType ?? "image/png"
231
+ });
232
+ }
233
+ }
234
+ return out;
235
+ }
236
+ function createGeminiAdapter(baseUrl) {
237
+ return {
238
+ ...GEMINI_META,
239
+ buildGenerate(req) {
240
+ const { generationConfig: _generationConfig, ...params } = req.params;
241
+ const body = {
242
+ contents: [{ parts: [{ text: req.prompt }] }],
243
+ ...params,
244
+ generationConfig: baseGenerationConfig(req)
245
+ };
246
+ return { method: "POST", url: `${baseUrl}/models/${req.model}:generateContent`, headers: {}, body };
247
+ },
248
+ buildEdit(req, images, _mask) {
249
+ const { generationConfig: _generationConfig, ...params } = req.params;
250
+ const parts = [{ text: req.prompt }];
251
+ for (const img of images) {
252
+ parts.push({ inline_data: { mime_type: img.mime, data: bytesToB64(img.bytes) } });
253
+ }
254
+ const body = {
255
+ contents: [{ parts }],
256
+ ...params,
257
+ generationConfig: baseGenerationConfig(req)
258
+ };
259
+ return { method: "POST", url: `${baseUrl}/models/${req.model}:generateContent`, headers: {}, body };
260
+ },
261
+ parseResponse: parseResponse2
262
+ };
263
+ }
264
+
265
+ // src/adapters/jsonpath.ts
266
+ function extractPath(obj, path) {
267
+ const tokens = path.replace(/\[(\d+|\*)\]/g, ".$1").split(".").filter(Boolean);
268
+ let current = [obj];
269
+ for (const token of tokens) {
270
+ const next = [];
271
+ for (const node of current) {
272
+ if (node == null) continue;
273
+ if (token === "*") {
274
+ if (Array.isArray(node)) next.push(...node);
275
+ } else if (/^\d+$/.test(token)) {
276
+ if (Array.isArray(node)) {
277
+ const value = node[Number(token)];
278
+ if (value !== void 0) next.push(value);
279
+ }
280
+ } else {
281
+ const value = node[token];
282
+ if (value !== void 0) next.push(value);
283
+ }
284
+ }
285
+ current = next;
286
+ }
287
+ return current;
288
+ }
289
+
290
+ // src/adapters/custom.ts
291
+ var CUSTOM_META = {
292
+ format: "custom",
293
+ defaultAuth: { style: "bearer" },
294
+ extraParams: {}
295
+ };
296
+ function escapeTemplateString(value) {
297
+ return JSON.stringify(value).slice(1, -1);
298
+ }
299
+ function render(template, req) {
300
+ const filled = template.replace(/\{\{prompt\}\}/g, escapeTemplateString(req.prompt)).replace(/\{\{model\}\}/g, escapeTemplateString(req.model)).replace(/\{\{size\}\}/g, escapeTemplateString(req.size ?? "")).replace(/\{\{n\}\}/g, String(req.n));
301
+ try {
302
+ return JSON.parse(filled);
303
+ } catch (e) {
304
+ throw new ConfigError(`\u6E32\u67D3 IMAGEN_CUSTOM_BODY_TEMPLATE \u540E\u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${e.message}`);
305
+ }
306
+ }
307
+ function appendFormValue2(fd, key, value) {
308
+ if (value === void 0 || value === null) return;
309
+ if (value instanceof Blob) {
310
+ fd.set(key, value);
311
+ return;
312
+ }
313
+ fd.set(key, typeof value === "object" ? JSON.stringify(value) : String(value));
314
+ }
315
+ function encodeBody(encoding, fields, params) {
316
+ const merged = { ...fields, ...params };
317
+ if (encoding === "json") return merged;
318
+ const fd = new FormData();
319
+ for (const [key, value] of Object.entries(merged)) {
320
+ appendFormValue2(fd, key, value);
321
+ }
322
+ return fd;
323
+ }
324
+ function createCustomAdapter(baseUrl, custom) {
325
+ const kind = custom.responseImageKind ?? "url";
326
+ return {
327
+ ...CUSTOM_META,
328
+ supportsEdit: Boolean(custom.editPath),
329
+ defaultBaseUrl: void 0,
330
+ buildGenerate(req) {
331
+ if (!custom.generatePath) throw new ConfigError("custom \u683C\u5F0F\u7F3A\u5C11 IMAGEN_CUSTOM_GENERATE_PATH");
332
+ if (!custom.bodyTemplate) throw new ConfigError("custom \u683C\u5F0F\u7F3A\u5C11 IMAGEN_CUSTOM_BODY_TEMPLATE");
333
+ const body = encodeBody(custom.encoding, render(custom.bodyTemplate, req), req.params);
334
+ return { method: "POST", url: `${baseUrl}${custom.generatePath}`, headers: {}, body };
335
+ },
336
+ buildEdit(req, images, _mask) {
337
+ if (!custom.editPath) throw new ConfigError("custom \u683C\u5F0F\u672A\u914D\u7F6E IMAGEN_CUSTOM_EDIT_PATH\uFF0C\u4E0D\u652F\u6301 edit");
338
+ if (!custom.bodyTemplate) throw new ConfigError("custom \u683C\u5F0F\u7F3A\u5C11 IMAGEN_CUSTOM_BODY_TEMPLATE");
339
+ const b64 = Buffer.from(images[0]?.bytes ?? new Uint8Array()).toString("base64");
340
+ const template = custom.bodyTemplate.replace(/\{\{image\}\}/g, escapeTemplateString(b64));
341
+ const body = encodeBody(custom.encoding, render(template, req), req.params);
342
+ return { method: "POST", url: `${baseUrl}${custom.editPath}`, headers: {}, body };
343
+ },
344
+ parseResponse(raw) {
345
+ if (!custom.responseImagesPath) throw new ConfigError("custom \u683C\u5F0F\u7F3A\u5C11 IMAGEN_CUSTOM_RESPONSE_IMAGES_PATH");
346
+ const values = extractPath(raw, custom.responseImagesPath);
347
+ return values.filter((value) => typeof value === "string").map((value) => {
348
+ if (kind === "url") return { kind: "url", data: value };
349
+ if (kind === "dataurl") {
350
+ const match = value.match(/^data:([^;]+);base64,(.+)$/s);
351
+ return { kind: "b64", data: match ? match[2] : value, mime: match?.[1] };
352
+ }
353
+ return { kind: "b64", data: value };
354
+ });
355
+ }
356
+ };
357
+ }
358
+
359
+ // src/adapters/registry.ts
360
+ function createAdapter(raw) {
361
+ if (raw.format === "openai") {
362
+ const baseUrl = resolveBaseUrl(raw, OPENAI_META.defaultBaseUrl);
363
+ return { adapter: createOpenAiAdapter(baseUrl), baseUrl, auth: resolveAuth(raw, OPENAI_META.defaultAuth) };
364
+ }
365
+ if (raw.format === "gemini") {
366
+ const baseUrl = resolveBaseUrl(raw, GEMINI_META.defaultBaseUrl);
367
+ return { adapter: createGeminiAdapter(baseUrl), baseUrl, auth: resolveAuth(raw, GEMINI_META.defaultAuth) };
368
+ }
369
+ if (raw.format === "custom") {
370
+ const baseUrl = resolveBaseUrl(raw, void 0);
371
+ return { adapter: createCustomAdapter(baseUrl, raw.custom), baseUrl, auth: resolveAuth(raw, CUSTOM_META.defaultAuth) };
372
+ }
373
+ throw new ConfigError(`\u672A\u77E5 format / IMAGEN_FORMAT "${raw.format}"\uFF0C\u53D7\u652F\u6301\uFF1Aopenai | gemini | custom`);
374
+ }
375
+
376
+ // src/adapters/overrides.ts
377
+ function applyOverrides(spec, extra) {
378
+ return {
379
+ ...spec,
380
+ headers: { ...spec.headers, ...extra.headers },
381
+ query: { ...spec.query ?? {}, ...extra.query }
382
+ };
383
+ }
384
+
385
+ // src/http.ts
386
+ function sleep(ms) {
387
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
388
+ }
389
+ function shouldRetryStatus(status) {
390
+ return status === 429 || status >= 500;
391
+ }
392
+ function appendQuery(url, query) {
393
+ const out = new URL(url);
394
+ for (const [key, value] of Object.entries(query ?? {})) {
395
+ out.searchParams.set(key, value);
396
+ }
397
+ return out.toString();
398
+ }
399
+ function applyAuth(url, headers, auth) {
400
+ if (auth.style === "none" || !auth.apiKey) return url;
401
+ if (auth.style === "bearer") {
402
+ headers.Authorization = `Bearer ${auth.apiKey}`;
403
+ return url;
404
+ }
405
+ if (auth.style === "header") {
406
+ headers[auth.headerName ?? "Authorization"] = auth.apiKey;
407
+ return url;
408
+ }
409
+ const queryName = auth.queryName ?? "key";
410
+ return appendQuery(url, { [queryName]: auth.apiKey });
411
+ }
412
+ function bodyAndHeaders(body, headers) {
413
+ if (body instanceof FormData) return body;
414
+ if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
415
+ headers["Content-Type"] = "application/json";
416
+ }
417
+ return JSON.stringify(body);
418
+ }
419
+ async function withTimeoutFetch(url, init, timeoutMs) {
420
+ const controller = new AbortController();
421
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
422
+ try {
423
+ return await fetch(url, { ...init, signal: controller.signal });
424
+ } finally {
425
+ clearTimeout(timer);
426
+ }
427
+ }
428
+ async function retrying(maxRetries, operation) {
429
+ let lastError;
430
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
431
+ try {
432
+ return await operation(attempt);
433
+ } catch (e) {
434
+ lastError = e;
435
+ if (e instanceof ProviderError && !shouldRetryStatus(e.status ?? 0)) break;
436
+ if (attempt === maxRetries) break;
437
+ await sleep(Math.min(50 * 2 ** attempt, 1e3));
438
+ }
439
+ }
440
+ throw lastError;
441
+ }
442
+ async function sendRequest(spec, auth, opts) {
443
+ return retrying(opts.maxRetries, async () => {
444
+ const headers = { ...spec.headers };
445
+ let url = appendQuery(spec.url, spec.query);
446
+ url = applyAuth(url, headers, auth);
447
+ const response = await withTimeoutFetch(
448
+ url,
449
+ {
450
+ method: spec.method,
451
+ headers,
452
+ body: bodyAndHeaders(spec.body, headers)
453
+ },
454
+ opts.timeoutMs
455
+ );
456
+ if (!response.ok) {
457
+ const providerBody = redactKey(await response.text(), auth.apiKey);
458
+ throw new ProviderError(friendlyHttpError(response.status, providerBody), response.status);
459
+ }
460
+ return response.json();
461
+ });
462
+ }
463
+ async function downloadToBytes(url, opts) {
464
+ return retrying(opts.maxRetries, async () => {
465
+ const response = await withTimeoutFetch(url, { method: "GET" }, opts.timeoutMs);
466
+ if (!response.ok) {
467
+ throw new ProviderError(`\u56FE\u50CF\u4E0B\u8F7D\u5931\u8D25 (HTTP ${response.status})\uFF1A${await response.text()}`, response.status);
468
+ }
469
+ const bytes = new Uint8Array(await response.arrayBuffer());
470
+ const mime = response.headers.get("content-type")?.split(";")[0] || "application/octet-stream";
471
+ return { bytes, mime };
472
+ });
473
+ }
474
+
475
+ // src/input.ts
476
+ import { readFile } from "fs/promises";
477
+ function sniffMime(bytes) {
478
+ if (bytes[0] === 137 && bytes[1] === 80) return "image/png";
479
+ if (bytes[0] === 255 && bytes[1] === 216) return "image/jpeg";
480
+ if (bytes[0] === 71 && bytes[1] === 73) return "image/gif";
481
+ if (bytes[0] === 82 && bytes[1] === 73 && bytes[8] === 87) return "image/webp";
482
+ return "application/octet-stream";
483
+ }
484
+ var DATA_URL_RE = /^data:([^;]+);base64,(.+)$/s;
485
+ async function resolveImage(ref, timeoutMs) {
486
+ const dataMatch = ref.match(DATA_URL_RE);
487
+ if (dataMatch) {
488
+ const bytes = new Uint8Array(Buffer.from(dataMatch[2], "base64"));
489
+ return { bytes, mime: dataMatch[1] };
490
+ }
491
+ if (/^https?:\/\//.test(ref)) {
492
+ return downloadToBytes(ref, { timeoutMs, maxRetries: 0 });
493
+ }
494
+ const compact = ref.replace(/\s/g, "");
495
+ if (/^[A-Za-z0-9+/=]+$/.test(compact) && compact.length % 4 === 0 && compact.length > 16) {
496
+ const bytes = new Uint8Array(Buffer.from(compact, "base64"));
497
+ if (bytes.length > 0) return { bytes, mime: sniffMime(bytes) };
498
+ }
499
+ try {
500
+ const buf = await readFile(ref);
501
+ const bytes = new Uint8Array(buf);
502
+ return { bytes, mime: sniffMime(bytes) };
503
+ } catch {
504
+ throw new ConfigError(`\u65E0\u6CD5\u89E3\u6790\u8F93\u5165\u56FE\uFF1A${ref}\uFF08\u65E2\u975E data URL / http URL / base64\uFF0C\u4E5F\u975E\u53EF\u8BFB\u6587\u4EF6\uFF09`);
505
+ }
506
+ }
507
+
508
+ // src/output.ts
509
+ import { mkdir, stat, writeFile } from "fs/promises";
510
+ import { basename, dirname, extname, join, resolve } from "path";
511
+ function extFromMime(mime) {
512
+ if (mime.includes("jpeg") || mime.includes("jpg")) return "jpg";
513
+ if (mime.includes("webp")) return "webp";
514
+ if (mime.includes("gif")) return "gif";
515
+ if (mime.includes("png")) return "png";
516
+ return "png";
517
+ }
518
+ function mimeFromOutputFormat(format) {
519
+ if (format !== "png" && format !== "jpeg" && format !== "jpg" && format !== "webp") return void 0;
520
+ return format === "jpg" || format === "jpeg" ? "image/jpeg" : `image/${format}`;
521
+ }
522
+ async function materialize(images, opts) {
523
+ const out = [];
524
+ const preferredMime = mimeFromOutputFormat(opts.preferredOutputFormat);
525
+ for (const image of images) {
526
+ if (image.kind === "b64") {
527
+ out.push({
528
+ bytes: new Uint8Array(Buffer.from(image.data, "base64")),
529
+ mime: image.mime ?? preferredMime ?? "image/png"
530
+ });
531
+ } else {
532
+ out.push(await downloadToBytes(image.data, opts));
533
+ }
534
+ }
535
+ return out;
536
+ }
537
+ async function isDir(path) {
538
+ try {
539
+ return (await stat(path)).isDirectory();
540
+ } catch {
541
+ return false;
542
+ }
543
+ }
544
+ async function saveImages(mats, opts) {
545
+ const paths = [];
546
+ const multi = mats.length > 1;
547
+ let targetDir = opts.outputDir;
548
+ let baseName;
549
+ if (opts.outputPath) {
550
+ const outputPath = resolve(opts.outputPath);
551
+ if (opts.outputPath.endsWith("/") || opts.outputPath.endsWith("\\") || await isDir(outputPath)) {
552
+ targetDir = outputPath;
553
+ } else {
554
+ targetDir = dirname(outputPath);
555
+ baseName = basename(outputPath);
556
+ }
557
+ }
558
+ await mkdir(targetDir, { recursive: true });
559
+ const stamp = Date.now();
560
+ for (let i = 0; i < mats.length; i += 1) {
561
+ const ext = extFromMime(mats[i].mime);
562
+ let name;
563
+ if (baseName) {
564
+ if (multi) {
565
+ const currentExt = extname(baseName);
566
+ const base = baseName.slice(0, baseName.length - currentExt.length);
567
+ name = `${base}-${i}${currentExt || `.${ext}`}`;
568
+ } else {
569
+ name = baseName;
570
+ }
571
+ } else {
572
+ name = `imagen-${stamp}-${i}.${ext}`;
573
+ }
574
+ const full = join(targetDir, name);
575
+ await writeFile(full, mats[i].bytes);
576
+ paths.push(resolve(full));
577
+ }
578
+ return paths;
579
+ }
580
+ function buildToolResult(paths, mats, meta, returnInline) {
581
+ const lines = [
582
+ `\u5DF2\u751F\u6210 ${paths.length} \u5F20\u56FE\u50CF\uFF08provider=${meta.provider}, model=${meta.model}${meta.size ? `, size=${meta.size}` : ""}\uFF09\uFF1A`,
583
+ ...paths.map((path) => `- ${path}`)
584
+ ];
585
+ const content = [{ type: "text", text: lines.join("\n") }];
586
+ if (returnInline) {
587
+ for (const mat of mats) {
588
+ content.push({ type: "image", data: Buffer.from(mat.bytes).toString("base64"), mimeType: mat.mime });
589
+ }
590
+ }
591
+ return { content, isError: false };
592
+ }
593
+
594
+ // src/server.ts
595
+ var coreShape = {
596
+ prompt: z2.string().describe("\u56FE\u50CF\u63CF\u8FF0/\u7F16\u8F91\u6307\u4EE4"),
597
+ model: z2.string().optional().describe("\u8986\u76D6\u9ED8\u8BA4\u6A21\u578B IMAGEN_MODEL"),
598
+ size: z2.string().optional().describe("\u5982 1024x1024\uFF1B\u9002\u914D\u5668\u81EA\u52A8\u6620\u5C04"),
599
+ n: z2.number().int().optional().describe("\u751F\u6210\u5F20\u6570\uFF0C\u9ED8\u8BA4 1"),
600
+ output_path: z2.string().optional().describe("\u4FDD\u5B58\u6587\u4EF6\u540D\u6216\u76EE\u5F55\uFF0C\u8986\u76D6\u9ED8\u8BA4\u8F93\u51FA\u76EE\u5F55")
601
+ };
602
+ var CORE_KEYS = /* @__PURE__ */ new Set(["prompt", "model", "size", "n", "output_path", "images", "mask"]);
603
+ function defaultDir() {
604
+ return join2(tmpdir(), "imagen-switch");
605
+ }
606
+ function toNormReq(args, raw) {
607
+ const model = args.model ?? raw.model;
608
+ if (!model) throw new ConfigError("\u672A\u63D0\u4F9B model\uFF0C\u4E14\u672A\u8BBE\u7F6E IMAGEN_MODEL");
609
+ const extras = {};
610
+ for (const [key, value] of Object.entries(args)) {
611
+ if (!CORE_KEYS.has(key) && value !== void 0) extras[key] = value;
612
+ }
613
+ return {
614
+ prompt: args.prompt,
615
+ model,
616
+ size: args.size,
617
+ n: args.n ?? 1,
618
+ params: { ...raw.extraBody, ...extras }
619
+ };
620
+ }
621
+ function errorResult(error, apiKey) {
622
+ const message = error instanceof Error ? error.message : String(error);
623
+ return { content: [{ type: "text", text: redactKey(message, apiKey) }], isError: true };
624
+ }
625
+ function buildServer(raw) {
626
+ const { adapter, auth } = createAdapter(raw);
627
+ const server = new McpServer({ name: "imagen-switch", version: "0.1.0" });
628
+ const opts = { timeoutMs: raw.timeoutMs, maxRetries: raw.maxRetries };
629
+ const overrides = { headers: raw.extraHeaders, query: raw.extraQuery };
630
+ server.registerTool(
631
+ "generate_image",
632
+ {
633
+ title: "Generate Image",
634
+ description: "\u7528\u914D\u7F6E\u7684 Provider \u751F\u6210\u56FE\u50CF\uFF0C\u4FDD\u5B58\u5230\u672C\u5730\u5E76\u8FD4\u56DE\u8DEF\u5F84\u3002",
635
+ inputSchema: { ...coreShape, ...adapter.extraParams }
636
+ },
637
+ async (args) => {
638
+ try {
639
+ const req = toNormReq(args, raw);
640
+ const spec = applyOverrides(adapter.buildGenerate(req), overrides);
641
+ const rawResponse = await sendRequest(spec, auth, opts);
642
+ const images = adapter.parseResponse(rawResponse);
643
+ if (images.length === 0) throw new ProviderError("Provider \u672A\u8FD4\u56DE\u4EFB\u4F55\u56FE\u50CF");
644
+ const mats = await materialize(images, { ...opts, preferredOutputFormat: req.params.output_format });
645
+ const paths = await saveImages(mats, {
646
+ outputDir: raw.outputDir ?? defaultDir(),
647
+ outputPath: args.output_path
648
+ });
649
+ return buildToolResult(paths, mats, { model: req.model, size: req.size, provider: adapter.format }, raw.returnInline);
650
+ } catch (e) {
651
+ return errorResult(e, auth.apiKey);
652
+ }
653
+ }
654
+ );
655
+ if (adapter.supportsEdit) {
656
+ server.registerTool(
657
+ "edit_image",
658
+ {
659
+ title: "Edit Image",
660
+ description: "\u57FA\u4E8E\u4E00\u5F20\u6216\u591A\u5F20\u8F93\u5165\u56FE\u6309\u6307\u4EE4\u7F16\u8F91/\u91CD\u7ED8\uFF0C\u4FDD\u5B58\u5230\u672C\u5730\u5E76\u8FD4\u56DE\u8DEF\u5F84\u3002",
661
+ inputSchema: {
662
+ ...coreShape,
663
+ images: z2.array(z2.string()).describe("\u8F93\u5165\u56FE\uFF1A\u672C\u5730\u8DEF\u5F84 / data URL / \u88F8 base64 / http(s) URL"),
664
+ mask: z2.string().optional().describe("inpaint \u8499\u7248\uFF08\u652F\u6301\u7684 Provider \u624D\u7528\uFF09"),
665
+ ...adapter.extraParams
666
+ }
667
+ },
668
+ async (args) => {
669
+ try {
670
+ const req = toNormReq(args, raw);
671
+ const refs = args.images ?? [];
672
+ if (refs.length === 0) throw new ConfigError("edit_image \u9700\u8981\u81F3\u5C11\u4E00\u5F20 images");
673
+ const images = await Promise.all(refs.map((ref) => resolveImage(ref, opts.timeoutMs)));
674
+ const mask = args.mask ? await resolveImage(args.mask, opts.timeoutMs) : void 0;
675
+ const spec = applyOverrides(adapter.buildEdit(req, images, mask), overrides);
676
+ const rawResponse = await sendRequest(spec, auth, opts);
677
+ const out = adapter.parseResponse(rawResponse);
678
+ if (out.length === 0) throw new ProviderError("Provider \u672A\u8FD4\u56DE\u4EFB\u4F55\u56FE\u50CF");
679
+ const mats = await materialize(out, { ...opts, preferredOutputFormat: req.params.output_format });
680
+ const paths = await saveImages(mats, {
681
+ outputDir: raw.outputDir ?? defaultDir(),
682
+ outputPath: args.output_path
683
+ });
684
+ return buildToolResult(paths, mats, { model: req.model, size: req.size, provider: adapter.format }, raw.returnInline);
685
+ } catch (e) {
686
+ return errorResult(e, auth.apiKey);
687
+ }
688
+ }
689
+ );
690
+ }
691
+ return server;
692
+ }
693
+
694
+ // src/index.ts
695
+ async function main() {
696
+ let raw;
697
+ try {
698
+ raw = loadRawConfig(process.env);
699
+ } catch (e) {
700
+ process.stderr.write(`[imagen-switch] \u914D\u7F6E\u9519\u8BEF\uFF1A${e instanceof Error ? e.message : String(e)}
701
+ `);
702
+ process.exit(1);
703
+ }
704
+ const server = buildServer(raw);
705
+ const transport = new StdioServerTransport();
706
+ await server.connect(transport);
707
+ process.stderr.write(`[imagen-switch] started (format=${raw.format})
708
+ `);
709
+ process.on("SIGINT", async () => {
710
+ await server.close();
711
+ process.exit(0);
712
+ });
713
+ }
714
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
715
+ main().catch((e) => {
716
+ process.stderr.write(`[imagen-switch] fatal: ${e instanceof Error ? e.message : String(e)}
717
+ `);
718
+ process.exit(1);
719
+ });
720
+ }
721
+ export {
722
+ main
723
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "imagen-switch-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for multi-provider image generation via env config",
5
+ "type": "module",
6
+ "bin": {
7
+ "imagen-switch-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "ci": "npm test && npm run typecheck && npm run build",
18
+ "prepublishOnly": "npm run ci",
19
+ "publish:dry-run": "npm publish --dry-run",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "license": "MIT",
25
+ "keywords": [
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "image-generation",
29
+ "openai",
30
+ "gemini",
31
+ "imagen",
32
+ "agent"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "1.29.0",
39
+ "zod": "^3.25.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.0.0",
43
+ "tsup": "^8.0.0",
44
+ "typescript": "^5.4.0",
45
+ "vitest": "^2.0.0"
46
+ }
47
+ }