@chrischall/gemini-mcp 0.4.0 → 0.5.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +216 -49
- package/dist/client.js +34 -36
- package/dist/tools/shared.js +8 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Google Gemini image generation and editing — generate and edit images via the Gemini API",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.5.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Gemini Images",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Generate and edit images with Google Gemini image models (Nano Banana / Nano Banana Pro) via natural language prompts",
|
|
18
|
-
"version": "0.
|
|
18
|
+
"version": "0.5.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/bundle.js
CHANGED
|
@@ -32095,20 +32095,9 @@ var init_server3 = __esm({
|
|
|
32095
32095
|
}
|
|
32096
32096
|
});
|
|
32097
32097
|
|
|
32098
|
-
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
32099
|
-
function textResult(data) {
|
|
32100
|
-
return {
|
|
32101
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
32102
|
-
};
|
|
32103
|
-
}
|
|
32104
|
-
var init_response = __esm({
|
|
32105
|
-
"node_modules/@chrischall/mcp-utils/dist/response/index.js"() {
|
|
32106
|
-
}
|
|
32107
|
-
});
|
|
32108
|
-
|
|
32109
32098
|
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
32110
32099
|
function redactSecrets(text) {
|
|
32111
|
-
return text.replace(BEARER_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
|
|
32100
|
+
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
|
|
32112
32101
|
}
|
|
32113
32102
|
function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
32114
32103
|
const str = text === null || text === void 0 ? "" : String(text);
|
|
@@ -32117,7 +32106,7 @@ function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
|
32117
32106
|
return redacted;
|
|
32118
32107
|
return `${redacted.slice(0, max)}\u2026 [truncated]`;
|
|
32119
32108
|
}
|
|
32120
|
-
var DEFAULT_ERROR_MESSAGE_MAX, McpToolError, BEARER_RE, JWT_RE;
|
|
32109
|
+
var DEFAULT_ERROR_MESSAGE_MAX, McpToolError, BEARER_RE, JWT_RE, BASIC_AUTH_RE, SET_COOKIE_RE, COOKIE_HEADER_RE, API_KEY_RE, QUERY_SECRET_RE;
|
|
32121
32110
|
var init_errors4 = __esm({
|
|
32122
32111
|
"node_modules/@chrischall/mcp-utils/dist/errors/index.js"() {
|
|
32123
32112
|
DEFAULT_ERROR_MESSAGE_MAX = 500;
|
|
@@ -32134,6 +32123,36 @@ var init_errors4 = __esm({
|
|
|
32134
32123
|
};
|
|
32135
32124
|
BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
32136
32125
|
JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
32126
|
+
BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
|
|
32127
|
+
SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
|
|
32128
|
+
COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
|
|
32129
|
+
API_KEY_RE = new RegExp([
|
|
32130
|
+
"sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
|
|
32131
|
+
// OpenAI / Anthropic (incl. sk-ant-…)
|
|
32132
|
+
"gh[pousr]_[A-Za-z0-9]{36,}\\b",
|
|
32133
|
+
// GitHub ghp_/gho_/ghu_/ghs_/ghr_
|
|
32134
|
+
"xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
|
|
32135
|
+
// Slack
|
|
32136
|
+
"AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
|
|
32137
|
+
// Google API key (39 chars total)
|
|
32138
|
+
"AKIA[0-9A-Z]{16}\\b",
|
|
32139
|
+
// AWS access key id (20 chars total)
|
|
32140
|
+
"whsec_[A-Za-z0-9]{16,}\\b"
|
|
32141
|
+
// webhook signing secret (Stripe-style)
|
|
32142
|
+
].map((p) => `\\b${p}`).join("|"), "g");
|
|
32143
|
+
QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
32144
|
+
}
|
|
32145
|
+
});
|
|
32146
|
+
|
|
32147
|
+
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
32148
|
+
function textResult(data) {
|
|
32149
|
+
return {
|
|
32150
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
32151
|
+
};
|
|
32152
|
+
}
|
|
32153
|
+
var init_response = __esm({
|
|
32154
|
+
"node_modules/@chrischall/mcp-utils/dist/response/index.js"() {
|
|
32155
|
+
init_errors4();
|
|
32137
32156
|
}
|
|
32138
32157
|
});
|
|
32139
32158
|
|
|
@@ -32178,10 +32197,168 @@ var init_fs = __esm({
|
|
|
32178
32197
|
}
|
|
32179
32198
|
});
|
|
32180
32199
|
|
|
32200
|
+
// node_modules/@chrischall/mcp-utils/dist/http/throttle.js
|
|
32201
|
+
var init_throttle = __esm({
|
|
32202
|
+
"node_modules/@chrischall/mcp-utils/dist/http/throttle.js"() {
|
|
32203
|
+
}
|
|
32204
|
+
});
|
|
32205
|
+
|
|
32181
32206
|
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
32207
|
+
function hostOf(baseUrl) {
|
|
32208
|
+
try {
|
|
32209
|
+
return new URL(baseUrl).host;
|
|
32210
|
+
} catch {
|
|
32211
|
+
return baseUrl;
|
|
32212
|
+
}
|
|
32213
|
+
}
|
|
32214
|
+
function createApiClient(opts) {
|
|
32215
|
+
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
32216
|
+
const retry = opts.retry ?? DEFAULT_RETRY;
|
|
32217
|
+
const service = opts.serviceName ?? hostOf(opts.baseUrl);
|
|
32218
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
32219
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
32220
|
+
const unauthorized = () => opts.onUnauthorized ? opts.onUnauthorized() : new UnauthorizedError(service);
|
|
32221
|
+
const rateLimited = () => opts.onRateLimited ? opts.onRateLimited() : new RateLimitedError(service);
|
|
32222
|
+
const retryStatuses = retry.statuses ?? [429];
|
|
32223
|
+
const timeoutMs = opts.timeout;
|
|
32224
|
+
const authHeader = (token) => !token ? {} : opts.tokenHeader ? { [opts.tokenHeader]: token } : { Authorization: `Bearer ${token}` };
|
|
32225
|
+
function withTimeout(run) {
|
|
32226
|
+
if (timeoutMs == null || timeoutMs <= 0)
|
|
32227
|
+
return run(void 0);
|
|
32228
|
+
const controller = new AbortController();
|
|
32229
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
32230
|
+
return run(controller.signal).catch((err) => {
|
|
32231
|
+
if (err instanceof Error && err.name === "AbortError")
|
|
32232
|
+
throw new RequestTimeoutError(service, timeoutMs);
|
|
32233
|
+
throw err;
|
|
32234
|
+
}).finally(() => clearTimeout(timer));
|
|
32235
|
+
}
|
|
32236
|
+
async function send(method, path, opt) {
|
|
32237
|
+
const isMultipart = opt.formData !== void 0;
|
|
32238
|
+
const hasJsonBody = !isMultipart && opt.body !== void 0;
|
|
32239
|
+
const reqBody = isMultipart ? opt.formData : hasJsonBody ? JSON.stringify(opt.body) : void 0;
|
|
32240
|
+
const query = opt.query ? buildQueryString(opt.query) : "";
|
|
32241
|
+
const url2 = `${base}${path}${query}`;
|
|
32242
|
+
const bodyInit = reqBody !== void 0 ? { body: reqBody } : {};
|
|
32243
|
+
const fetchWith = (token, signal) => doFetch(url2, {
|
|
32244
|
+
method,
|
|
32245
|
+
headers: {
|
|
32246
|
+
Accept: "application/json",
|
|
32247
|
+
...hasJsonBody ? { "Content-Type": "application/json" } : {},
|
|
32248
|
+
...opts.baseHeaders,
|
|
32249
|
+
...authHeader(token || void 0),
|
|
32250
|
+
...opt.headers
|
|
32251
|
+
},
|
|
32252
|
+
...signal ? { signal } : {},
|
|
32253
|
+
...bodyInit
|
|
32254
|
+
});
|
|
32255
|
+
const once = () => withTimeout((signal) => opts.tokenManager ? opts.tokenManager.withAuth((token) => fetchWith(token, signal)) : (async () => fetchWith(await opts.getToken?.() || void 0, signal))());
|
|
32256
|
+
let attempt = 0;
|
|
32257
|
+
for (; ; ) {
|
|
32258
|
+
const res = await once();
|
|
32259
|
+
if (retryStatuses.includes(res.status) && attempt < retry.count) {
|
|
32260
|
+
attempt += 1;
|
|
32261
|
+
await sleep(retry.delayMs);
|
|
32262
|
+
continue;
|
|
32263
|
+
}
|
|
32264
|
+
return res;
|
|
32265
|
+
}
|
|
32266
|
+
}
|
|
32267
|
+
async function fetchJson(method, path, opt = {}) {
|
|
32268
|
+
const res = await send(method, path, opt);
|
|
32269
|
+
if (res.status === 401)
|
|
32270
|
+
throw unauthorized();
|
|
32271
|
+
if (res.status === 429)
|
|
32272
|
+
throw rateLimited();
|
|
32273
|
+
if (res.status === 204)
|
|
32274
|
+
return void 0;
|
|
32275
|
+
const text = await res.text();
|
|
32276
|
+
if (!res.ok) {
|
|
32277
|
+
throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
|
|
32278
|
+
}
|
|
32279
|
+
if (text.length === 0)
|
|
32280
|
+
return void 0;
|
|
32281
|
+
return JSON.parse(text);
|
|
32282
|
+
}
|
|
32283
|
+
async function fetchHtml(method, path, opt = {}) {
|
|
32284
|
+
const headers = { Accept: "text/html,*/*", ...opt.headers };
|
|
32285
|
+
const res = await send(method, path, { ...opt, headers });
|
|
32286
|
+
if (res.status === 401)
|
|
32287
|
+
throw unauthorized();
|
|
32288
|
+
if (res.status === 429)
|
|
32289
|
+
throw rateLimited();
|
|
32290
|
+
const text = await res.text();
|
|
32291
|
+
if (!res.ok) {
|
|
32292
|
+
throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
|
|
32293
|
+
}
|
|
32294
|
+
return text;
|
|
32295
|
+
}
|
|
32296
|
+
return { fetchJson, fetchHtml };
|
|
32297
|
+
}
|
|
32298
|
+
function buildQueryString(params) {
|
|
32299
|
+
const parts = [];
|
|
32300
|
+
for (const [key, value] of Object.entries(params)) {
|
|
32301
|
+
if (value === void 0 || value === null || value === "")
|
|
32302
|
+
continue;
|
|
32303
|
+
if (Array.isArray(value)) {
|
|
32304
|
+
for (const item of value) {
|
|
32305
|
+
if (item === void 0 || item === null || item === "")
|
|
32306
|
+
continue;
|
|
32307
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`);
|
|
32308
|
+
}
|
|
32309
|
+
} else {
|
|
32310
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
32311
|
+
}
|
|
32312
|
+
}
|
|
32313
|
+
return parts.length > 0 ? `?${parts.join("&")}` : "";
|
|
32314
|
+
}
|
|
32315
|
+
function formatApiError(status, method, path, errorText, opts = {}) {
|
|
32316
|
+
const service = opts.service ?? "API";
|
|
32317
|
+
const head = `${service} error ${status} for ${method.toUpperCase()} ${path}`;
|
|
32318
|
+
const safe = truncateErrorMessage(errorText ?? "", opts.max).trim();
|
|
32319
|
+
return safe.length > 0 ? `${head}: ${safe}` : head;
|
|
32320
|
+
}
|
|
32321
|
+
var DEFAULT_RETRY, defaultSleep, UnauthorizedError, RateLimitedError, RequestTimeoutError, ApiError;
|
|
32182
32322
|
var init_http = __esm({
|
|
32183
32323
|
"node_modules/@chrischall/mcp-utils/dist/http/index.js"() {
|
|
32184
32324
|
init_errors4();
|
|
32325
|
+
init_throttle();
|
|
32326
|
+
DEFAULT_RETRY = { count: 1, delayMs: 2e3 };
|
|
32327
|
+
defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
32328
|
+
UnauthorizedError = class extends Error {
|
|
32329
|
+
status = 401;
|
|
32330
|
+
constructor(service) {
|
|
32331
|
+
super(`Unauthorized (401) from ${service} \u2014 the token is missing, invalid, or expired.`);
|
|
32332
|
+
this.name = "UnauthorizedError";
|
|
32333
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
32334
|
+
}
|
|
32335
|
+
};
|
|
32336
|
+
RateLimitedError = class extends Error {
|
|
32337
|
+
status = 429;
|
|
32338
|
+
constructor(service) {
|
|
32339
|
+
super(`Rate limited (429) by ${service} after retries.`);
|
|
32340
|
+
this.name = "RateLimitedError";
|
|
32341
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
32342
|
+
}
|
|
32343
|
+
};
|
|
32344
|
+
RequestTimeoutError = class extends Error {
|
|
32345
|
+
timeoutMs;
|
|
32346
|
+
constructor(service, timeoutMs) {
|
|
32347
|
+
super(`Request to ${service} timed out after ${timeoutMs}ms.`);
|
|
32348
|
+
this.name = "RequestTimeoutError";
|
|
32349
|
+
this.timeoutMs = timeoutMs;
|
|
32350
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
32351
|
+
}
|
|
32352
|
+
};
|
|
32353
|
+
ApiError = class extends Error {
|
|
32354
|
+
status;
|
|
32355
|
+
constructor(status, message) {
|
|
32356
|
+
super(message);
|
|
32357
|
+
this.name = "ApiError";
|
|
32358
|
+
this.status = status;
|
|
32359
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
32360
|
+
}
|
|
32361
|
+
};
|
|
32185
32362
|
}
|
|
32186
32363
|
});
|
|
32187
32364
|
|
|
@@ -32302,7 +32479,7 @@ var init_clipboard = __esm({
|
|
|
32302
32479
|
init_dist();
|
|
32303
32480
|
|
|
32304
32481
|
// src/version.ts
|
|
32305
|
-
var VERSION = "0.
|
|
32482
|
+
var VERSION = "0.5.0";
|
|
32306
32483
|
|
|
32307
32484
|
// src/tools/models.ts
|
|
32308
32485
|
init_dist();
|
|
@@ -32329,12 +32506,15 @@ function filterImageModels(raw) {
|
|
|
32329
32506
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32330
32507
|
await loadDotenvSafely({ path: join(__dirname, "..", ".env"), override: false });
|
|
32331
32508
|
var BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
32332
|
-
var INTERACTIONS_URL = "https://generativelanguage.googleapis.com/v1beta/interactions";
|
|
32333
32509
|
var SERVICE = "Gemini";
|
|
32510
|
+
var INTERACTIONS_SERVICE = "Gemini Interactions";
|
|
32511
|
+
var API_REVISION = "2026-05-20";
|
|
32512
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
32334
32513
|
var GeminiClient = class {
|
|
32335
32514
|
apiKey;
|
|
32336
32515
|
configError;
|
|
32337
|
-
|
|
32516
|
+
api;
|
|
32517
|
+
interactionsApi;
|
|
32338
32518
|
constructor(opts = {}) {
|
|
32339
32519
|
const key = readEnvVar("GEMINI_API_KEY");
|
|
32340
32520
|
if (!key) {
|
|
@@ -32346,7 +32526,19 @@ var GeminiClient = class {
|
|
|
32346
32526
|
this.apiKey = key;
|
|
32347
32527
|
this.configError = null;
|
|
32348
32528
|
}
|
|
32349
|
-
|
|
32529
|
+
const shared = {
|
|
32530
|
+
tokenHeader: "x-goog-api-key",
|
|
32531
|
+
getToken: () => this.requireKey(),
|
|
32532
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
32533
|
+
fetchImpl: opts.fetchImpl ?? fetch
|
|
32534
|
+
};
|
|
32535
|
+
this.api = createApiClient({ baseUrl: BASE_URL, serviceName: SERVICE, ...shared });
|
|
32536
|
+
this.interactionsApi = createApiClient({
|
|
32537
|
+
baseUrl: BASE_URL,
|
|
32538
|
+
serviceName: INTERACTIONS_SERVICE,
|
|
32539
|
+
baseHeaders: { "Api-Revision": API_REVISION },
|
|
32540
|
+
...shared
|
|
32541
|
+
});
|
|
32350
32542
|
}
|
|
32351
32543
|
requireKey() {
|
|
32352
32544
|
if (this.configError) throw this.configError;
|
|
@@ -32357,17 +32549,7 @@ var GeminiClient = class {
|
|
|
32357
32549
|
return resolveModel(void 0, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
32358
32550
|
}
|
|
32359
32551
|
async call(method, path, body) {
|
|
32360
|
-
|
|
32361
|
-
const res = await this.fetchImpl(`${BASE_URL}${path}`, {
|
|
32362
|
-
method,
|
|
32363
|
-
headers: { "x-goog-api-key": key, "content-type": "application/json" },
|
|
32364
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
32365
|
-
});
|
|
32366
|
-
if (!res.ok) {
|
|
32367
|
-
const text = await res.text().catch(() => "");
|
|
32368
|
-
throw new McpToolError(`${SERVICE} API ${res.status}: ${truncateErrorMessage(text)}`);
|
|
32369
|
-
}
|
|
32370
|
-
return await res.json();
|
|
32552
|
+
return this.api.fetchJson(method, path, body !== void 0 ? { body } : {});
|
|
32371
32553
|
}
|
|
32372
32554
|
async listModels() {
|
|
32373
32555
|
const data = await this.call("GET", "/models?pageSize=200");
|
|
@@ -32431,7 +32613,6 @@ var GeminiClient = class {
|
|
|
32431
32613
|
return { images, text, grounding };
|
|
32432
32614
|
}
|
|
32433
32615
|
async interact(opts) {
|
|
32434
|
-
const key = this.requireKey();
|
|
32435
32616
|
const model = resolveModel(opts.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
32436
32617
|
const inputParts = [{ type: "text", text: opts.input }];
|
|
32437
32618
|
for (const img of opts.images ?? []) {
|
|
@@ -32447,26 +32628,9 @@ var GeminiClient = class {
|
|
|
32447
32628
|
if (opts.thinkingLevel !== void 0) body.generation_config = { thinking_level: opts.thinkingLevel };
|
|
32448
32629
|
if (opts.previousInteractionId !== void 0) body.previous_interaction_id = opts.previousInteractionId;
|
|
32449
32630
|
if (opts.googleSearch) body.tools = [{ type: "google_search" }];
|
|
32450
|
-
const
|
|
32451
|
-
|
|
32452
|
-
headers: {
|
|
32453
|
-
"x-goog-api-key": key,
|
|
32454
|
-
"content-type": "application/json",
|
|
32455
|
-
"Api-Revision": "2026-05-20"
|
|
32456
|
-
},
|
|
32457
|
-
body: JSON.stringify(body)
|
|
32631
|
+
const data = await this.interactionsApi.fetchJson("POST", "/interactions", {
|
|
32632
|
+
body
|
|
32458
32633
|
});
|
|
32459
|
-
if (!res.ok) {
|
|
32460
|
-
const text = await res.text().catch(() => "");
|
|
32461
|
-
let message = text;
|
|
32462
|
-
try {
|
|
32463
|
-
const parsed = JSON.parse(text);
|
|
32464
|
-
if (parsed.error?.message) message = parsed.error.message;
|
|
32465
|
-
} catch {
|
|
32466
|
-
}
|
|
32467
|
-
throw new McpToolError(`Gemini Interactions API ${res.status}: ${truncateErrorMessage(message)}`);
|
|
32468
|
-
}
|
|
32469
|
-
const data = await res.json();
|
|
32470
32634
|
const images = [];
|
|
32471
32635
|
const textParts = [];
|
|
32472
32636
|
const searchQueries = [];
|
|
@@ -32621,7 +32785,10 @@ init_dist();
|
|
|
32621
32785
|
var ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1"];
|
|
32622
32786
|
var IMAGE_SIZES = ["512", "1K", "2K", "4K"];
|
|
32623
32787
|
var sharedImageSchema = {
|
|
32624
|
-
|
|
32788
|
+
// Bare id only: the model is interpolated into the URL path
|
|
32789
|
+
// (`/models/${model}:generateContent`), so slashes/colons/queries must be
|
|
32790
|
+
// rejected to keep a crafted value from escaping the path segment.
|
|
32791
|
+
model: external_exports.string().regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")').optional().describe("Model id override (default: server default; see gemini_list_models)"),
|
|
32625
32792
|
aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
|
|
32626
32793
|
image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
|
|
32627
32794
|
output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
|
package/dist/client.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { loadDotenvSafely, readEnvVar, McpToolError,
|
|
3
|
+
import { loadDotenvSafely, readEnvVar, McpToolError, createApiClient } from '@chrischall/mcp-utils';
|
|
4
4
|
import { resolveModel, filterImageModels } from './models.js';
|
|
5
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
await loadDotenvSafely({ path: join(__dirname, '..', '.env'), override: false });
|
|
7
7
|
const BASE_URL = 'https://generativelanguage.googleapis.com/v1beta'; // v1 lacks gemini-3-pro-image; confirmed via Task 5
|
|
8
|
-
const INTERACTIONS_URL = 'https://generativelanguage.googleapis.com/v1beta/interactions';
|
|
9
8
|
const SERVICE = 'Gemini';
|
|
9
|
+
const INTERACTIONS_SERVICE = 'Gemini Interactions';
|
|
10
|
+
// Required revision header for the beta Interactions API (verified 2026-06-08;
|
|
11
|
+
// see docs/GEMINI-API.md) — load-bearing, requests 400 without it.
|
|
12
|
+
const API_REVISION = '2026-05-20';
|
|
13
|
+
// Per-request abort budget. Image generation routinely runs 30s+ on the Pro
|
|
14
|
+
// model, so this is deliberately 60s (NOT the fleet's usual 15–30s).
|
|
15
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
10
16
|
export class GeminiClient {
|
|
11
17
|
apiKey;
|
|
12
18
|
configError;
|
|
13
|
-
|
|
19
|
+
api;
|
|
20
|
+
interactionsApi;
|
|
14
21
|
constructor(opts = {}) {
|
|
15
22
|
const key = readEnvVar('GEMINI_API_KEY');
|
|
16
23
|
if (!key) {
|
|
@@ -23,7 +30,27 @@ export class GeminiClient {
|
|
|
23
30
|
this.apiKey = key;
|
|
24
31
|
this.configError = null;
|
|
25
32
|
}
|
|
26
|
-
|
|
33
|
+
// Both paths share: the key in the `x-goog-api-key` header (Gemini does not
|
|
34
|
+
// use `Authorization: Bearer`), a 60s abort budget, and the default 429
|
|
35
|
+
// retry (once after 2s — the free tier rate-limits aggressively).
|
|
36
|
+
// `getToken` calls requireKey() so key resolution stays at REQUEST time:
|
|
37
|
+
// the server boots without a key and the actionable config error (with its
|
|
38
|
+
// aistudio hint) surfaces on the first tool call, not at startup.
|
|
39
|
+
const shared = {
|
|
40
|
+
tokenHeader: 'x-goog-api-key',
|
|
41
|
+
getToken: () => this.requireKey(),
|
|
42
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
43
|
+
fetchImpl: opts.fetchImpl ?? fetch,
|
|
44
|
+
};
|
|
45
|
+
this.api = createApiClient({ baseUrl: BASE_URL, serviceName: SERVICE, ...shared });
|
|
46
|
+
// Separate client for the beta Interactions API: same base URL, but every
|
|
47
|
+
// request must carry the Api-Revision header (and errors name the API).
|
|
48
|
+
this.interactionsApi = createApiClient({
|
|
49
|
+
baseUrl: BASE_URL,
|
|
50
|
+
serviceName: INTERACTIONS_SERVICE,
|
|
51
|
+
baseHeaders: { 'Api-Revision': API_REVISION },
|
|
52
|
+
...shared,
|
|
53
|
+
});
|
|
27
54
|
}
|
|
28
55
|
requireKey() {
|
|
29
56
|
if (this.configError)
|
|
@@ -35,17 +62,7 @@ export class GeminiClient {
|
|
|
35
62
|
return resolveModel(undefined, readEnvVar('GEMINI_IMAGE_MODEL'));
|
|
36
63
|
}
|
|
37
64
|
async call(method, path, body) {
|
|
38
|
-
|
|
39
|
-
const res = await this.fetchImpl(`${BASE_URL}${path}`, {
|
|
40
|
-
method,
|
|
41
|
-
headers: { 'x-goog-api-key': key, 'content-type': 'application/json' },
|
|
42
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
43
|
-
});
|
|
44
|
-
if (!res.ok) {
|
|
45
|
-
const text = await res.text().catch(() => '');
|
|
46
|
-
throw new McpToolError(`${SERVICE} API ${res.status}: ${truncateErrorMessage(text)}`);
|
|
47
|
-
}
|
|
48
|
-
return (await res.json());
|
|
65
|
+
return this.api.fetchJson(method, path, body !== undefined ? { body } : {});
|
|
49
66
|
}
|
|
50
67
|
async listModels() {
|
|
51
68
|
const data = await this.call('GET', '/models?pageSize=200');
|
|
@@ -115,7 +132,6 @@ export class GeminiClient {
|
|
|
115
132
|
return { images, text, grounding };
|
|
116
133
|
}
|
|
117
134
|
async interact(opts) {
|
|
118
|
-
const key = this.requireKey();
|
|
119
135
|
const model = resolveModel(opts.model, readEnvVar('GEMINI_IMAGE_MODEL'));
|
|
120
136
|
const inputParts = [{ type: 'text', text: opts.input }];
|
|
121
137
|
for (const img of opts.images ?? []) {
|
|
@@ -136,27 +152,9 @@ export class GeminiClient {
|
|
|
136
152
|
body.previous_interaction_id = opts.previousInteractionId;
|
|
137
153
|
if (opts.googleSearch)
|
|
138
154
|
body.tools = [{ type: 'google_search' }];
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
headers: {
|
|
142
|
-
'x-goog-api-key': key,
|
|
143
|
-
'content-type': 'application/json',
|
|
144
|
-
'Api-Revision': '2026-05-20',
|
|
145
|
-
},
|
|
146
|
-
body: JSON.stringify(body),
|
|
155
|
+
const data = await this.interactionsApi.fetchJson('POST', '/interactions', {
|
|
156
|
+
body,
|
|
147
157
|
});
|
|
148
|
-
if (!res.ok) {
|
|
149
|
-
const text = await res.text().catch(() => '');
|
|
150
|
-
let message = text;
|
|
151
|
-
try {
|
|
152
|
-
const parsed = JSON.parse(text);
|
|
153
|
-
if (parsed.error?.message)
|
|
154
|
-
message = parsed.error.message;
|
|
155
|
-
}
|
|
156
|
-
catch { /* use raw text */ }
|
|
157
|
-
throw new McpToolError(`Gemini Interactions API ${res.status}: ${truncateErrorMessage(message)}`);
|
|
158
|
-
}
|
|
159
|
-
const data = await res.json();
|
|
160
158
|
// Only surface the `model_output` step — that's the caller-facing result.
|
|
161
159
|
// `thought` steps hold internal reasoning (and, with includeThoughts, draft
|
|
162
160
|
// "thinking" images); collecting those would leak reasoning into `text` and
|
package/dist/tools/shared.js
CHANGED
|
@@ -11,7 +11,14 @@ export const IMAGE_SIZES = ['512', '1K', '2K', '4K'];
|
|
|
11
11
|
* into each tool's `inputSchema`.
|
|
12
12
|
*/
|
|
13
13
|
export const sharedImageSchema = {
|
|
14
|
-
|
|
14
|
+
// Bare id only: the model is interpolated into the URL path
|
|
15
|
+
// (`/models/${model}:generateContent`), so slashes/colons/queries must be
|
|
16
|
+
// rejected to keep a crafted value from escaping the path segment.
|
|
17
|
+
model: z
|
|
18
|
+
.string()
|
|
19
|
+
.regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")')
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Model id override (default: server default; see gemini_list_models)'),
|
|
15
22
|
aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe('Output aspect ratio'),
|
|
16
23
|
image_size: z.enum(IMAGE_SIZES).optional().describe('Output resolution (512 = 0.5K, Flash-only)'),
|
|
17
24
|
output_dir: z.string().optional().describe('Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)'),
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Single source of the server version. release-please bumps the literal below. */
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.5.0'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/gemini-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/gemini-mcp",
|
|
5
5
|
"description": "Gemini image-generation MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"test:coverage": "vitest run --coverage"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@chrischall/mcp-utils": "^0.
|
|
47
|
+
"@chrischall/mcp-utils": "^0.7.0",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
49
|
"dotenv": "^17.4.0",
|
|
50
50
|
"zod": "^4.4.2"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/gemini-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.5.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.5.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|