@chrischall/gemini-mcp 0.7.2 → 0.9.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/README.md +21 -0
- package/dist/bundle.js +594 -139
- package/dist/client.js +55 -29
- package/dist/images.js +42 -1
- package/dist/index.js +2 -1
- package/dist/jobs.js +0 -0
- package/dist/tools/_confirm.js +39 -0
- package/dist/tools/generate.js +108 -64
- package/dist/tools/interact.js +92 -41
- package/dist/tools/jobs.js +17 -0
- package/dist/tools/set.js +63 -37
- package/dist/tools/shared.js +132 -8
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/bundle.js
CHANGED
|
@@ -32106,7 +32106,7 @@ var init_server3 = __esm({
|
|
|
32106
32106
|
|
|
32107
32107
|
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
32108
32108
|
function redactSecrets(text) {
|
|
32109
|
-
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]");
|
|
32109
|
+
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(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
32110
32110
|
}
|
|
32111
32111
|
function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
32112
32112
|
const str = text === null || text === void 0 ? "" : String(text);
|
|
@@ -32115,7 +32115,7 @@ function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
|
32115
32115
|
return redacted;
|
|
32116
32116
|
return `${redacted.slice(0, max)}\u2026 [truncated]`;
|
|
32117
32117
|
}
|
|
32118
|
-
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;
|
|
32118
|
+
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, AWS_SIGV4_RE, JSON_SECRET_KEYS, JSON_SECRET_DQ_RE, JSON_SECRET_SQ_RE;
|
|
32119
32119
|
var init_errors4 = __esm({
|
|
32120
32120
|
"node_modules/@chrischall/mcp-utils/dist/errors/index.js"() {
|
|
32121
32121
|
DEFAULT_ERROR_MESSAGE_MAX = 500;
|
|
@@ -32150,6 +32150,10 @@ var init_errors4 = __esm({
|
|
|
32150
32150
|
// webhook signing secret (Stripe-style)
|
|
32151
32151
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
32152
32152
|
QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
32153
|
+
AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
|
|
32154
|
+
JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
32155
|
+
JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
32156
|
+
JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
32153
32157
|
}
|
|
32154
32158
|
});
|
|
32155
32159
|
|
|
@@ -32200,6 +32204,13 @@ var init_config = __esm({
|
|
|
32200
32204
|
}
|
|
32201
32205
|
});
|
|
32202
32206
|
|
|
32207
|
+
// node_modules/@chrischall/mcp-utils/dist/fs/output.js
|
|
32208
|
+
var init_output = __esm({
|
|
32209
|
+
"node_modules/@chrischall/mcp-utils/dist/fs/output.js"() {
|
|
32210
|
+
init_config();
|
|
32211
|
+
}
|
|
32212
|
+
});
|
|
32213
|
+
|
|
32203
32214
|
// node_modules/@chrischall/mcp-utils/dist/fs/index.js
|
|
32204
32215
|
import { openAsBlob } from "node:fs";
|
|
32205
32216
|
async function fileBlob(path, opts = {}) {
|
|
@@ -32216,6 +32227,7 @@ async function fileBlob(path, opts = {}) {
|
|
|
32216
32227
|
}
|
|
32217
32228
|
var init_fs = __esm({
|
|
32218
32229
|
"node_modules/@chrischall/mcp-utils/dist/fs/index.js"() {
|
|
32230
|
+
init_output();
|
|
32219
32231
|
}
|
|
32220
32232
|
});
|
|
32221
32233
|
|
|
@@ -32225,6 +32237,28 @@ var init_throttle = __esm({
|
|
|
32225
32237
|
}
|
|
32226
32238
|
});
|
|
32227
32239
|
|
|
32240
|
+
// node_modules/@chrischall/mcp-utils/dist/http/response-cache.js
|
|
32241
|
+
var init_response_cache = __esm({
|
|
32242
|
+
"node_modules/@chrischall/mcp-utils/dist/http/response-cache.js"() {
|
|
32243
|
+
}
|
|
32244
|
+
});
|
|
32245
|
+
|
|
32246
|
+
// node_modules/@chrischall/mcp-utils/dist/http/net-atoms.js
|
|
32247
|
+
function parseRetryAfterMs(header, opts = {}) {
|
|
32248
|
+
const defaultMs = opts.defaultMs ?? 2e3;
|
|
32249
|
+
const capMs = opts.capMs ?? 3e4;
|
|
32250
|
+
if (header == null)
|
|
32251
|
+
return defaultMs;
|
|
32252
|
+
const trimmed = header.trim();
|
|
32253
|
+
if (!/^\d+$/.test(trimmed))
|
|
32254
|
+
return defaultMs;
|
|
32255
|
+
return Math.min(Number(trimmed) * 1e3, capMs);
|
|
32256
|
+
}
|
|
32257
|
+
var init_net_atoms = __esm({
|
|
32258
|
+
"node_modules/@chrischall/mcp-utils/dist/http/net-atoms.js"() {
|
|
32259
|
+
}
|
|
32260
|
+
});
|
|
32261
|
+
|
|
32228
32262
|
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
32229
32263
|
function hostOf(baseUrl) {
|
|
32230
32264
|
try {
|
|
@@ -32280,7 +32314,11 @@ function createApiClient(opts) {
|
|
|
32280
32314
|
const res = await once();
|
|
32281
32315
|
if (retryStatuses.includes(res.status) && attempt < retry.count) {
|
|
32282
32316
|
attempt += 1;
|
|
32283
|
-
|
|
32317
|
+
const delay = retry.honorRetryAfter ? parseRetryAfterMs(res.headers.get("retry-after"), {
|
|
32318
|
+
defaultMs: retry.delayMs,
|
|
32319
|
+
capMs: retry.maxRetryAfterMs ?? 3e4
|
|
32320
|
+
}) : retry.delayMs;
|
|
32321
|
+
await sleep(delay);
|
|
32284
32322
|
continue;
|
|
32285
32323
|
}
|
|
32286
32324
|
return res;
|
|
@@ -32315,7 +32353,26 @@ function createApiClient(opts) {
|
|
|
32315
32353
|
}
|
|
32316
32354
|
return text;
|
|
32317
32355
|
}
|
|
32318
|
-
|
|
32356
|
+
async function fetchRaw(method, path, opt = {}) {
|
|
32357
|
+
const headers = { Accept: "*/*", ...opt.headers };
|
|
32358
|
+
const res = await send(method, path, { ...opt, headers });
|
|
32359
|
+
if (res.status === 401)
|
|
32360
|
+
throw unauthorized();
|
|
32361
|
+
if (res.status === 429)
|
|
32362
|
+
throw rateLimited();
|
|
32363
|
+
if (!res.ok) {
|
|
32364
|
+
const text = await res.text().catch(() => "");
|
|
32365
|
+
throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
|
|
32366
|
+
}
|
|
32367
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
32368
|
+
return {
|
|
32369
|
+
status: res.status,
|
|
32370
|
+
contentType: res.headers.get("content-type"),
|
|
32371
|
+
headers: res.headers,
|
|
32372
|
+
bytes
|
|
32373
|
+
};
|
|
32374
|
+
}
|
|
32375
|
+
return { fetchJson, fetchHtml, fetchRaw };
|
|
32319
32376
|
}
|
|
32320
32377
|
function buildQueryString(params) {
|
|
32321
32378
|
const parts = [];
|
|
@@ -32345,6 +32402,9 @@ var init_http = __esm({
|
|
|
32345
32402
|
"node_modules/@chrischall/mcp-utils/dist/http/index.js"() {
|
|
32346
32403
|
init_errors4();
|
|
32347
32404
|
init_throttle();
|
|
32405
|
+
init_response_cache();
|
|
32406
|
+
init_net_atoms();
|
|
32407
|
+
init_net_atoms();
|
|
32348
32408
|
DEFAULT_RETRY = { count: 1, delayMs: 2e3 };
|
|
32349
32409
|
defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
32350
32410
|
UnauthorizedError = class extends Error {
|
|
@@ -32384,9 +32444,16 @@ var init_http = __esm({
|
|
|
32384
32444
|
}
|
|
32385
32445
|
});
|
|
32386
32446
|
|
|
32447
|
+
// node_modules/@chrischall/mcp-utils/dist/concurrency/single-flight.js
|
|
32448
|
+
var init_single_flight = __esm({
|
|
32449
|
+
"node_modules/@chrischall/mcp-utils/dist/concurrency/single-flight.js"() {
|
|
32450
|
+
}
|
|
32451
|
+
});
|
|
32452
|
+
|
|
32387
32453
|
// node_modules/@chrischall/mcp-utils/dist/concurrency/index.js
|
|
32388
32454
|
var init_concurrency = __esm({
|
|
32389
32455
|
"node_modules/@chrischall/mcp-utils/dist/concurrency/index.js"() {
|
|
32456
|
+
init_single_flight();
|
|
32390
32457
|
}
|
|
32391
32458
|
});
|
|
32392
32459
|
|
|
@@ -32396,11 +32463,19 @@ var init_dates = __esm({
|
|
|
32396
32463
|
}
|
|
32397
32464
|
});
|
|
32398
32465
|
|
|
32466
|
+
// node_modules/@chrischall/mcp-utils/dist/zod/parse-lenient.js
|
|
32467
|
+
var init_parse_lenient = __esm({
|
|
32468
|
+
"node_modules/@chrischall/mcp-utils/dist/zod/parse-lenient.js"() {
|
|
32469
|
+
init_errors4();
|
|
32470
|
+
}
|
|
32471
|
+
});
|
|
32472
|
+
|
|
32399
32473
|
// node_modules/@chrischall/mcp-utils/dist/zod/index.js
|
|
32400
32474
|
var PositiveInt, NonNegInt, NonEmptyString, IsoDate, IsoTime, NumericIdString, SafePathSegment, schemaOrigin, schemaConfirm, paginationSchema, pageSchema;
|
|
32401
32475
|
var init_zod2 = __esm({
|
|
32402
32476
|
"node_modules/@chrischall/mcp-utils/dist/zod/index.js"() {
|
|
32403
32477
|
init_zod();
|
|
32478
|
+
init_parse_lenient();
|
|
32404
32479
|
PositiveInt = external_exports.number().int().positive();
|
|
32405
32480
|
NonNegInt = external_exports.number().int().nonnegative();
|
|
32406
32481
|
NonEmptyString = external_exports.string().min(1);
|
|
@@ -32421,12 +32496,32 @@ var init_zod2 = __esm({
|
|
|
32421
32496
|
}
|
|
32422
32497
|
});
|
|
32423
32498
|
|
|
32499
|
+
// node_modules/@chrischall/mcp-utils/dist/auth/cached-token.js
|
|
32500
|
+
var init_cached_token = __esm({
|
|
32501
|
+
"node_modules/@chrischall/mcp-utils/dist/auth/cached-token.js"() {
|
|
32502
|
+
}
|
|
32503
|
+
});
|
|
32504
|
+
|
|
32505
|
+
// node_modules/@chrischall/mcp-utils/dist/auth/es256.js
|
|
32506
|
+
var init_es256 = __esm({
|
|
32507
|
+
"node_modules/@chrischall/mcp-utils/dist/auth/es256.js"() {
|
|
32508
|
+
}
|
|
32509
|
+
});
|
|
32510
|
+
|
|
32424
32511
|
// node_modules/@chrischall/mcp-utils/dist/auth/index.js
|
|
32425
32512
|
var init_auth = __esm({
|
|
32426
32513
|
"node_modules/@chrischall/mcp-utils/dist/auth/index.js"() {
|
|
32427
32514
|
init_config();
|
|
32428
32515
|
init_errors4();
|
|
32429
32516
|
init_http();
|
|
32517
|
+
init_cached_token();
|
|
32518
|
+
init_es256();
|
|
32519
|
+
}
|
|
32520
|
+
});
|
|
32521
|
+
|
|
32522
|
+
// node_modules/@chrischall/mcp-utils/dist/scrape/index.js
|
|
32523
|
+
var init_scrape = __esm({
|
|
32524
|
+
"node_modules/@chrischall/mcp-utils/dist/scrape/index.js"() {
|
|
32430
32525
|
}
|
|
32431
32526
|
});
|
|
32432
32527
|
|
|
@@ -32443,6 +32538,7 @@ var init_dist = __esm({
|
|
|
32443
32538
|
init_dates();
|
|
32444
32539
|
init_zod2();
|
|
32445
32540
|
init_auth();
|
|
32541
|
+
init_scrape();
|
|
32446
32542
|
}
|
|
32447
32543
|
});
|
|
32448
32544
|
|
|
@@ -32510,7 +32606,7 @@ var init_clipboard = __esm({
|
|
|
32510
32606
|
init_dist();
|
|
32511
32607
|
|
|
32512
32608
|
// src/version.ts
|
|
32513
|
-
var VERSION = "0.
|
|
32609
|
+
var VERSION = "0.9.0";
|
|
32514
32610
|
|
|
32515
32611
|
// src/tools/models.ts
|
|
32516
32612
|
init_dist();
|
|
@@ -32540,6 +32636,13 @@ var BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
|
32540
32636
|
var SERVICE = "Gemini";
|
|
32541
32637
|
var INTERACTIONS_SERVICE = "Gemini Interactions";
|
|
32542
32638
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
32639
|
+
var REQUEST_TIMEOUT_4K_MS = 12e4;
|
|
32640
|
+
function resolveTimeoutMs(perCallMs, imageSize) {
|
|
32641
|
+
if (perCallMs !== void 0 && perCallMs > 0) return perCallMs;
|
|
32642
|
+
const env = Number(readEnvVar("GEMINI_TIMEOUT_MS"));
|
|
32643
|
+
if (Number.isFinite(env) && env > 0) return env;
|
|
32644
|
+
return imageSize === "4K" ? REQUEST_TIMEOUT_4K_MS : REQUEST_TIMEOUT_MS;
|
|
32645
|
+
}
|
|
32543
32646
|
var UPLOAD_BASE_URL = "https://generativelanguage.googleapis.com/upload/v1beta";
|
|
32544
32647
|
var FILE_MAX_BYTES = 2 * 1024 ** 3;
|
|
32545
32648
|
var FILE_POLL_INTERVAL_MS = 2e3;
|
|
@@ -32549,8 +32652,7 @@ var CHAIN_404_RETRY_MS = 2e3;
|
|
|
32549
32652
|
var GeminiClient = class {
|
|
32550
32653
|
apiKey;
|
|
32551
32654
|
configError;
|
|
32552
|
-
|
|
32553
|
-
interactionsApi;
|
|
32655
|
+
apis = /* @__PURE__ */ new Map();
|
|
32554
32656
|
fetchImpl;
|
|
32555
32657
|
sleep;
|
|
32556
32658
|
constructor(opts = {}) {
|
|
@@ -32566,18 +32668,37 @@ var GeminiClient = class {
|
|
|
32566
32668
|
}
|
|
32567
32669
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
32568
32670
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
32569
|
-
|
|
32570
|
-
|
|
32571
|
-
|
|
32572
|
-
|
|
32573
|
-
|
|
32574
|
-
|
|
32575
|
-
|
|
32576
|
-
|
|
32577
|
-
|
|
32578
|
-
|
|
32579
|
-
|
|
32580
|
-
|
|
32671
|
+
}
|
|
32672
|
+
/**
|
|
32673
|
+
* The `createApiClient` pair for one timeout budget, built lazily and
|
|
32674
|
+
* memoized per distinct timeout (a handful of values in practice). Both
|
|
32675
|
+
* share: the key in the `x-goog-api-key` header (Gemini does not use
|
|
32676
|
+
* `Authorization: Bearer`) and the default 429 retry (once after 2s — the
|
|
32677
|
+
* free tier rate-limits aggressively). `getToken` calls requireKey() so key
|
|
32678
|
+
* resolution stays at REQUEST time: the server boots without a key and the
|
|
32679
|
+
* actionable config error (with its aistudio hint) surfaces on the first
|
|
32680
|
+
* tool call, not at startup.
|
|
32681
|
+
*/
|
|
32682
|
+
apisFor(timeoutMs) {
|
|
32683
|
+
let entry = this.apis.get(timeoutMs);
|
|
32684
|
+
if (!entry) {
|
|
32685
|
+
const shared = {
|
|
32686
|
+
tokenHeader: "x-goog-api-key",
|
|
32687
|
+
getToken: () => this.requireKey(),
|
|
32688
|
+
timeout: timeoutMs,
|
|
32689
|
+
fetchImpl: this.fetchImpl
|
|
32690
|
+
};
|
|
32691
|
+
entry = {
|
|
32692
|
+
api: createApiClient({ baseUrl: BASE_URL, serviceName: SERVICE, ...shared }),
|
|
32693
|
+
// Separate client for the Interactions API so errors name the right API.
|
|
32694
|
+
// The beta-era Api-Revision header is gone: the API went GA and requests
|
|
32695
|
+
// succeed without it (verified live 2026-07-06; see docs/GEMINI-API.md).
|
|
32696
|
+
// Don't re-add a pinned revision — it would freeze us on old semantics.
|
|
32697
|
+
interactionsApi: createApiClient({ baseUrl: BASE_URL, serviceName: INTERACTIONS_SERVICE, ...shared })
|
|
32698
|
+
};
|
|
32699
|
+
this.apis.set(timeoutMs, entry);
|
|
32700
|
+
}
|
|
32701
|
+
return entry;
|
|
32581
32702
|
}
|
|
32582
32703
|
requireKey() {
|
|
32583
32704
|
if (this.configError) throw this.configError;
|
|
@@ -32587,8 +32708,9 @@ var GeminiClient = class {
|
|
|
32587
32708
|
defaultModel() {
|
|
32588
32709
|
return resolveModel(void 0, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
32589
32710
|
}
|
|
32590
|
-
async call(method, path, body) {
|
|
32591
|
-
|
|
32711
|
+
async call(method, path, body, timeoutMs) {
|
|
32712
|
+
const { api } = this.apisFor(timeoutMs ?? resolveTimeoutMs());
|
|
32713
|
+
return api.fetchJson(method, path, body !== void 0 ? { body } : {});
|
|
32592
32714
|
}
|
|
32593
32715
|
/**
|
|
32594
32716
|
* Upload a local video to the Gemini Files API (resumable protocol) and wait
|
|
@@ -32704,7 +32826,8 @@ var GeminiClient = class {
|
|
|
32704
32826
|
const data = await this.call(
|
|
32705
32827
|
"POST",
|
|
32706
32828
|
`/models/${model}:generateContent`,
|
|
32707
|
-
requestBody
|
|
32829
|
+
requestBody,
|
|
32830
|
+
resolveTimeoutMs(opts.timeoutMs, opts.imageSize)
|
|
32708
32831
|
);
|
|
32709
32832
|
const images = [];
|
|
32710
32833
|
const textParts = [];
|
|
@@ -32758,10 +32881,11 @@ var GeminiClient = class {
|
|
|
32758
32881
|
if (opts.searchTypes?.length) searchTool.search_types = opts.searchTypes;
|
|
32759
32882
|
body.tools = [searchTool];
|
|
32760
32883
|
}
|
|
32884
|
+
const { interactionsApi } = this.apisFor(resolveTimeoutMs(opts.timeoutMs, opts.imageSize));
|
|
32761
32885
|
let data;
|
|
32762
32886
|
for (let attempt = 0; ; attempt++) {
|
|
32763
32887
|
try {
|
|
32764
|
-
data = await
|
|
32888
|
+
data = await interactionsApi.fetchJson("POST", "/interactions", {
|
|
32765
32889
|
body
|
|
32766
32890
|
});
|
|
32767
32891
|
break;
|
|
@@ -32844,7 +32968,7 @@ init_dist();
|
|
|
32844
32968
|
|
|
32845
32969
|
// src/images.ts
|
|
32846
32970
|
init_dist();
|
|
32847
|
-
import { mkdir, readFile as readFile2, writeFile, access } from "node:fs/promises";
|
|
32971
|
+
import { mkdir, readFile as readFile2, writeFile, access, stat, open } from "node:fs/promises";
|
|
32848
32972
|
import { existsSync } from "node:fs";
|
|
32849
32973
|
import { join as join3, resolve, isAbsolute } from "node:path";
|
|
32850
32974
|
function slugify2(text, max = 40) {
|
|
@@ -32887,6 +33011,9 @@ async function writeImage(dir, base, base643, mimeType) {
|
|
|
32887
33011
|
await writeFile(path, Buffer.from(base643, "base64"));
|
|
32888
33012
|
return resolve(path);
|
|
32889
33013
|
}
|
|
33014
|
+
async function writeSidecar(imagePath, data) {
|
|
33015
|
+
await writeFile(`${imagePath}.json`, JSON.stringify(data, null, 2));
|
|
33016
|
+
}
|
|
32890
33017
|
function resolveInputPath(p, kind) {
|
|
32891
33018
|
if (isAbsolute(p) && existsSync(p)) return p;
|
|
32892
33019
|
const inputDir = readEnvVar("GEMINI_INPUT_DIR");
|
|
@@ -32930,6 +33057,23 @@ function videoMimeType(p) {
|
|
|
32930
33057
|
}
|
|
32931
33058
|
return mime;
|
|
32932
33059
|
}
|
|
33060
|
+
async function previewImageInput(path) {
|
|
33061
|
+
const resolved = resolveImagePath(path);
|
|
33062
|
+
const { size } = await stat(resolved);
|
|
33063
|
+
const fh = await open(resolved, "r");
|
|
33064
|
+
try {
|
|
33065
|
+
const head = Buffer.alloc(16);
|
|
33066
|
+
const { bytesRead } = await fh.read(head, 0, 16, 0);
|
|
33067
|
+
return { path: resolved, mimeType: sniffMimeBytes(head.subarray(0, bytesRead)), size };
|
|
33068
|
+
} finally {
|
|
33069
|
+
await fh.close();
|
|
33070
|
+
}
|
|
33071
|
+
}
|
|
33072
|
+
async function previewVideoInput(path) {
|
|
33073
|
+
const resolved = resolveVideoPath(path);
|
|
33074
|
+
const { size } = await stat(resolved);
|
|
33075
|
+
return { path: resolved, mimeType: videoMimeType(resolved), size };
|
|
33076
|
+
}
|
|
32933
33077
|
async function readImageAsInline(path) {
|
|
32934
33078
|
const resolved = resolveImagePath(path);
|
|
32935
33079
|
const buf = await readFile2(resolved);
|
|
@@ -32974,6 +33118,15 @@ init_dist();
|
|
|
32974
33118
|
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"];
|
|
32975
33119
|
var IMAGE_SIZES = ["512", "1K", "2K", "4K"];
|
|
32976
33120
|
var MODEL_CHOICE_GUIDE = "gemini-3.1-flash-image (Nano Banana 2) is the versatile generalist workhorse \u2014 balances speed with state-of-the-art 4K generation, world knowledge, and reliable text rendering; excels at multi-reference-image processing and consistency. gemini-3-pro-image (Nano Banana Pro) is the premium choice for the most complex visual tasks \u2014 highest world knowledge, advanced localization, accurate brand consistency, precision creative control. gemini-3.1-flash-lite-image (Nano Banana 2 Lite) is the fastest/cheapest for simple tasks (1K only, no search grounding).";
|
|
33121
|
+
var timeoutMsSchema = external_exports.number().int().positive().optional().describe(
|
|
33122
|
+
"Upstream request timeout in ms for this call (default: $GEMINI_TIMEOUT_MS, else 60000 \u2014 or 120000 when image_size is 4K, which routinely runs past 60s)"
|
|
33123
|
+
);
|
|
33124
|
+
var idempotencyKeySchema = external_exports.string().min(1).optional().describe(
|
|
33125
|
+
"Opaque idempotency key: a repeat call with the same key returns the recorded result (reused: true) instead of billing a new generation. Set it when retrying after a host timeout (-32001) to avoid a duplicate charge."
|
|
33126
|
+
);
|
|
33127
|
+
var asyncSchema = external_exports.boolean().optional().describe(
|
|
33128
|
+
"Run in the background and return a job_id immediately instead of the image, so a long (Pro/4K) generation cannot hit the host tools/call timeout (-32001). Poll gemini_get_result with the job_id to fetch the result (jobs are per-process and expire ~10 min after completion)."
|
|
33129
|
+
);
|
|
32977
33130
|
var sharedImageSchema = {
|
|
32978
33131
|
// Bare id only: the model is interpolated into the URL path
|
|
32979
33132
|
// (`/models/${model}:generateContent`), so slashes/colons/queries must be
|
|
@@ -32986,7 +33139,10 @@ var sharedImageSchema = {
|
|
|
32986
33139
|
seed: external_exports.number().int().optional().describe("Seed for reproducible generation; random if omitted"),
|
|
32987
33140
|
thinking_level: external_exports.enum(["minimal", "high"]).optional().describe("Reasoning depth (Gemini 3 models); higher can help complex/structural edits"),
|
|
32988
33141
|
google_search: external_exports.boolean().optional().describe("Ground the image in live Google Search results (current events, weather, data)"),
|
|
32989
|
-
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)")
|
|
33142
|
+
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)"),
|
|
33143
|
+
timeout_ms: timeoutMsSchema,
|
|
33144
|
+
idempotency_key: idempotencyKeySchema,
|
|
33145
|
+
async: asyncSchema
|
|
32990
33146
|
};
|
|
32991
33147
|
async function resolveVideoInput(args) {
|
|
32992
33148
|
if (args.video_url && args.video_path) {
|
|
@@ -33005,6 +33161,53 @@ var videoPathSchema = external_exports.string().optional().describe(
|
|
|
33005
33161
|
function pickSeed(seed) {
|
|
33006
33162
|
return seed ?? Math.floor(Math.random() * 2147483e3);
|
|
33007
33163
|
}
|
|
33164
|
+
var HEARTBEAT_DEFAULT_MS = 1e4;
|
|
33165
|
+
async function withProgressHeartbeat(extra, message, fn) {
|
|
33166
|
+
const progressToken = extra?._meta?.progressToken;
|
|
33167
|
+
const send = extra?.sendNotification;
|
|
33168
|
+
const env = Number(readEnvVar("GEMINI_HEARTBEAT_MS"));
|
|
33169
|
+
const intervalMs = Number.isFinite(env) ? env : HEARTBEAT_DEFAULT_MS;
|
|
33170
|
+
const debug = !!readEnvVar("GEMINI_DEBUG");
|
|
33171
|
+
const active = progressToken !== void 0 && !!send && intervalMs > 0;
|
|
33172
|
+
if (debug) {
|
|
33173
|
+
if (active) {
|
|
33174
|
+
console.error(`[gemini-mcp] heartbeat active: progressToken present, interval=${intervalMs}ms \u2014 ${message}`);
|
|
33175
|
+
} else {
|
|
33176
|
+
const reason = progressToken === void 0 ? "no progressToken from host (host cannot extend its tools/call timeout; long calls will hit it)" : !send ? "host provided no sendNotification channel" : "heartbeat disabled (GEMINI_HEARTBEAT_MS=0)";
|
|
33177
|
+
console.error(`[gemini-mcp] heartbeat inactive: ${reason} \u2014 ${message}`);
|
|
33178
|
+
}
|
|
33179
|
+
}
|
|
33180
|
+
if (progressToken === void 0 || !send || intervalMs <= 0) return fn();
|
|
33181
|
+
let ticks = 0;
|
|
33182
|
+
const timer = setInterval(() => {
|
|
33183
|
+
ticks++;
|
|
33184
|
+
const elapsed = Math.round(ticks * intervalMs / 1e3);
|
|
33185
|
+
if (debug) console.error(`[gemini-mcp] heartbeat: ${message} \u2014 still working (${elapsed}s)`);
|
|
33186
|
+
void send({
|
|
33187
|
+
method: "notifications/progress",
|
|
33188
|
+
params: {
|
|
33189
|
+
progressToken,
|
|
33190
|
+
progress: ticks,
|
|
33191
|
+
message: `${message} \u2014 still working (${elapsed}s)`
|
|
33192
|
+
}
|
|
33193
|
+
}).catch(() => {
|
|
33194
|
+
});
|
|
33195
|
+
}, intervalMs);
|
|
33196
|
+
timer.unref?.();
|
|
33197
|
+
try {
|
|
33198
|
+
return await fn();
|
|
33199
|
+
} finally {
|
|
33200
|
+
clearInterval(timer);
|
|
33201
|
+
}
|
|
33202
|
+
}
|
|
33203
|
+
function timeoutRiskHint(opts) {
|
|
33204
|
+
const proModel = /(?:^|[-_./])pro(?:[-_./]|$)/i.test(opts.model);
|
|
33205
|
+
const fourK = opts.imageSize === "4K";
|
|
33206
|
+
const multi = (opts.count ?? 1) > 1;
|
|
33207
|
+
if (!proModel && !fourK && !multi) return void 0;
|
|
33208
|
+
const why = [proModel && "Pro model", fourK && "4K output", multi && `count=${opts.count}`].filter(Boolean).join(" + ");
|
|
33209
|
+
return `Slow config (${why}) can exceed a host's tools/call timeout (e.g. Claude Desktop ~30s, error -32001). If the host gives up mid-generation the image is still written to disk (and gemini_interact leaves an <image>.json sidecar with the interaction id) \u2014 no rerun needed, just look in the output dir. For a faster in-band result use a Flash model, drop image_size to 1K/2K, or \u2014 in Claude Code \u2014 set MCP_TOOL_TIMEOUT.`;
|
|
33210
|
+
}
|
|
33008
33211
|
function buildMeta(model, seed, opts) {
|
|
33009
33212
|
const meta3 = { model, seed };
|
|
33010
33213
|
if (opts.aspect_ratio) meta3.aspect_ratio = opts.aspect_ratio;
|
|
@@ -33024,11 +33227,147 @@ async function emit(named, opts, meta3, onWritten) {
|
|
|
33024
33227
|
const dir = resolveOutputDir(opts.output_dir);
|
|
33025
33228
|
const images = [];
|
|
33026
33229
|
for (const n of named) images.push(await writeImage(dir, n.base, n.image.base64, n.image.mimeType));
|
|
33027
|
-
onWritten?.(images);
|
|
33230
|
+
await onWritten?.(images);
|
|
33028
33231
|
return textResult({ images, ...meta3 });
|
|
33029
33232
|
}
|
|
33030
33233
|
|
|
33234
|
+
// src/jobs.ts
|
|
33235
|
+
init_dist();
|
|
33236
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
33237
|
+
var JOB_TTL_MS = 6e5;
|
|
33238
|
+
var JOB_MAX = 64;
|
|
33239
|
+
var jobs = /* @__PURE__ */ new Map();
|
|
33240
|
+
var byKey = /* @__PURE__ */ new Map();
|
|
33241
|
+
var runningByFingerprint = /* @__PURE__ */ new Map();
|
|
33242
|
+
function isExpired(entry, now) {
|
|
33243
|
+
return entry.settledAt !== void 0 && now - entry.settledAt > JOB_TTL_MS;
|
|
33244
|
+
}
|
|
33245
|
+
function evict(now) {
|
|
33246
|
+
for (const [id, entry] of jobs) {
|
|
33247
|
+
if (isExpired(entry, now)) remove(id, entry);
|
|
33248
|
+
}
|
|
33249
|
+
if (jobs.size < JOB_MAX) return;
|
|
33250
|
+
const settled = [...jobs.values()].filter((e) => e.settledAt !== void 0).sort((a, b) => (a.settledAt ?? 0) - (b.settledAt ?? 0));
|
|
33251
|
+
for (const e of settled) {
|
|
33252
|
+
if (jobs.size < JOB_MAX) break;
|
|
33253
|
+
remove(e.jobId, e);
|
|
33254
|
+
}
|
|
33255
|
+
}
|
|
33256
|
+
function remove(id, entry) {
|
|
33257
|
+
jobs.delete(id);
|
|
33258
|
+
if (entry.idempotencyKey !== void 0 && byKey.get(entry.idempotencyKey) === id) byKey.delete(entry.idempotencyKey);
|
|
33259
|
+
if (runningByFingerprint.get(entry.fingerprint) === id) runningByFingerprint.delete(entry.fingerprint);
|
|
33260
|
+
}
|
|
33261
|
+
function fingerprintRequest(toolName, parts) {
|
|
33262
|
+
return createHash("sha256").update(`${toolName}\0${JSON.stringify(parts)}`).digest("hex");
|
|
33263
|
+
}
|
|
33264
|
+
function annotateReused(result, jobId) {
|
|
33265
|
+
const content = result.content ?? [];
|
|
33266
|
+
const first = content[0];
|
|
33267
|
+
if (first && first.type === "text") {
|
|
33268
|
+
let obj;
|
|
33269
|
+
try {
|
|
33270
|
+
obj = JSON.parse(first.text);
|
|
33271
|
+
} catch {
|
|
33272
|
+
obj = {};
|
|
33273
|
+
}
|
|
33274
|
+
obj.reused = true;
|
|
33275
|
+
obj.reused_job_id = jobId;
|
|
33276
|
+
return { ...result, content: [{ ...first, text: JSON.stringify(obj, null, 2) }, ...content.slice(1)] };
|
|
33277
|
+
}
|
|
33278
|
+
return {
|
|
33279
|
+
...result,
|
|
33280
|
+
content: [{ type: "text", text: JSON.stringify({ reused: true, reused_job_id: jobId }, null, 2) }, ...content]
|
|
33281
|
+
};
|
|
33282
|
+
}
|
|
33283
|
+
function jobHandle(jobId, status) {
|
|
33284
|
+
return textResult({
|
|
33285
|
+
job_id: jobId,
|
|
33286
|
+
status,
|
|
33287
|
+
hint: `Generation running in the background. Poll gemini_get_result with job_id "${jobId}" until status is "done" (results are per-process and expire ~10 min after completion).`
|
|
33288
|
+
});
|
|
33289
|
+
}
|
|
33290
|
+
async function dispatch(opts, work) {
|
|
33291
|
+
const { toolName, fingerprint, idempotencyKey, async } = opts;
|
|
33292
|
+
const now = Date.now();
|
|
33293
|
+
let hit;
|
|
33294
|
+
if (idempotencyKey !== void 0) {
|
|
33295
|
+
const id = byKey.get(idempotencyKey);
|
|
33296
|
+
const entry2 = id ? jobs.get(id) : void 0;
|
|
33297
|
+
if (entry2 && entry2.status !== "failed" && !isExpired(entry2, now)) hit = entry2;
|
|
33298
|
+
} else {
|
|
33299
|
+
const id = runningByFingerprint.get(fingerprint);
|
|
33300
|
+
const entry2 = id ? jobs.get(id) : void 0;
|
|
33301
|
+
if (entry2 && entry2.status === "running") hit = entry2;
|
|
33302
|
+
}
|
|
33303
|
+
if (hit) {
|
|
33304
|
+
if (async) return jobHandle(hit.jobId, hit.status);
|
|
33305
|
+
return annotateReused(await hit.promise, hit.jobId);
|
|
33306
|
+
}
|
|
33307
|
+
const jobId = randomUUID();
|
|
33308
|
+
evict(now);
|
|
33309
|
+
const promise2 = work();
|
|
33310
|
+
const entry = { jobId, toolName, fingerprint, idempotencyKey, status: "running", promise: promise2, createdAt: now };
|
|
33311
|
+
jobs.set(jobId, entry);
|
|
33312
|
+
if (idempotencyKey !== void 0) byKey.set(idempotencyKey, jobId);
|
|
33313
|
+
runningByFingerprint.set(fingerprint, jobId);
|
|
33314
|
+
promise2.then(
|
|
33315
|
+
(res) => {
|
|
33316
|
+
entry.status = "done";
|
|
33317
|
+
entry.result = res;
|
|
33318
|
+
entry.settledAt = Date.now();
|
|
33319
|
+
},
|
|
33320
|
+
(err) => {
|
|
33321
|
+
entry.status = "failed";
|
|
33322
|
+
entry.error = { message: err instanceof Error ? err.message : String(err), hint: err?.hint };
|
|
33323
|
+
entry.settledAt = Date.now();
|
|
33324
|
+
}
|
|
33325
|
+
).finally(() => {
|
|
33326
|
+
if (runningByFingerprint.get(fingerprint) === jobId) runningByFingerprint.delete(fingerprint);
|
|
33327
|
+
});
|
|
33328
|
+
if (async) return jobHandle(jobId, "running");
|
|
33329
|
+
return promise2;
|
|
33330
|
+
}
|
|
33331
|
+
function getJobResult(jobId) {
|
|
33332
|
+
const entry = jobs.get(jobId);
|
|
33333
|
+
if (!entry) {
|
|
33334
|
+
throw new McpToolError(`No job "${jobId}" \u2014 unknown or expired.`, {
|
|
33335
|
+
hint: "Jobs are per-process and evicted ~10 min after completion (or on server restart). If the generation likely finished, check the output dir / <image>.json sidecar instead of polling."
|
|
33336
|
+
});
|
|
33337
|
+
}
|
|
33338
|
+
if (entry.status === "running") return jobHandle(jobId, "running");
|
|
33339
|
+
if (entry.status === "failed") {
|
|
33340
|
+
throw new McpToolError(entry.error?.message ?? "Job failed.", entry.error?.hint ? { hint: entry.error.hint } : void 0);
|
|
33341
|
+
}
|
|
33342
|
+
if (!entry.result) throw new McpToolError(`Job "${jobId}" completed without a recorded result.`);
|
|
33343
|
+
return entry.result;
|
|
33344
|
+
}
|
|
33345
|
+
|
|
33346
|
+
// src/tools/_confirm.ts
|
|
33347
|
+
init_dist();
|
|
33348
|
+
function previewUnlessConfirmed(confirm, action, method, path, body) {
|
|
33349
|
+
if (confirm === true) return null;
|
|
33350
|
+
return textResult({
|
|
33351
|
+
dryRun: true,
|
|
33352
|
+
action,
|
|
33353
|
+
method,
|
|
33354
|
+
path,
|
|
33355
|
+
...body !== void 0 ? { willSend: body } : {},
|
|
33356
|
+
note: "Re-run with confirm: true to execute."
|
|
33357
|
+
});
|
|
33358
|
+
}
|
|
33359
|
+
async function previewLocalInputsUnlessConfirmed(confirm, action, endpoint, imagePaths, videoPath) {
|
|
33360
|
+
const hasLocal = (imagePaths?.length ?? 0) > 0 || Boolean(videoPath);
|
|
33361
|
+
if (confirm === true || !hasLocal) return null;
|
|
33362
|
+
const inputs = [];
|
|
33363
|
+
for (const p of imagePaths ?? []) inputs.push(await previewImageInput(p));
|
|
33364
|
+
if (videoPath) inputs.push(await previewVideoInput(videoPath));
|
|
33365
|
+
return previewUnlessConfirmed(confirm, action, "POST", endpoint, { inputs });
|
|
33366
|
+
}
|
|
33367
|
+
|
|
33031
33368
|
// src/tools/generate.ts
|
|
33369
|
+
var GENERATE_ENDPOINT = "/v1beta/models/{model}:generateContent";
|
|
33370
|
+
var SEND_LOCAL_ACTION = "Send local file input(s) to the Gemini API";
|
|
33032
33371
|
var REFINE_HINT = "To iteratively refine this image, use gemini_interact (pass the output file via `images` on the first call, then chain `previous_interaction_id` on each subsequent call).";
|
|
33033
33372
|
function registerGenerateTools(server) {
|
|
33034
33373
|
server.registerTool(
|
|
@@ -33044,43 +33383,67 @@ function registerGenerateTools(server) {
|
|
|
33044
33383
|
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs"),
|
|
33045
33384
|
video_url: external_exports.string().url().optional().describe("Public YouTube URL (or a previously uploaded Files API uri) as a video reference (video\u2192image; use a Flash model e.g. gemini-3.1-flash-image)"),
|
|
33046
33385
|
video_path: videoPathSchema,
|
|
33386
|
+
confirm: schemaConfirm,
|
|
33047
33387
|
...sharedImageSchema
|
|
33048
33388
|
}
|
|
33049
33389
|
},
|
|
33050
|
-
async (args) => {
|
|
33390
|
+
async (args, extra) => {
|
|
33391
|
+
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images, args.video_path);
|
|
33392
|
+
if (gate) return gate;
|
|
33051
33393
|
const count = args.count ?? 1;
|
|
33052
|
-
const seed = pickSeed(args.seed);
|
|
33053
33394
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33054
|
-
const
|
|
33055
|
-
|
|
33056
|
-
|
|
33057
|
-
|
|
33058
|
-
|
|
33059
|
-
|
|
33060
|
-
|
|
33061
|
-
|
|
33062
|
-
|
|
33063
|
-
|
|
33064
|
-
|
|
33065
|
-
|
|
33066
|
-
|
|
33067
|
-
|
|
33068
|
-
|
|
33069
|
-
|
|
33070
|
-
|
|
33071
|
-
|
|
33395
|
+
const fingerprint = fingerprintRequest("gemini_generate_image", {
|
|
33396
|
+
model,
|
|
33397
|
+
prompt: args.prompt,
|
|
33398
|
+
aspect_ratio: args.aspect_ratio,
|
|
33399
|
+
image_size: args.image_size,
|
|
33400
|
+
thinking_level: args.thinking_level,
|
|
33401
|
+
google_search: args.google_search,
|
|
33402
|
+
count,
|
|
33403
|
+
images: args.images,
|
|
33404
|
+
images_base64: args.images_base64,
|
|
33405
|
+
from_clipboard: args.from_clipboard,
|
|
33406
|
+
video_url: args.video_url,
|
|
33407
|
+
video_path: args.video_path
|
|
33408
|
+
});
|
|
33409
|
+
return dispatch({ toolName: "gemini_generate_image", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33410
|
+
const seed = pickSeed(args.seed);
|
|
33411
|
+
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33412
|
+
const refInputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
|
|
33413
|
+
const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args));
|
|
33414
|
+
const named = [];
|
|
33415
|
+
let capturedText;
|
|
33416
|
+
let capturedGrounding;
|
|
33417
|
+
await withProgressHeartbeat(extra, `Generating ${count > 1 ? `${count} images` : "image"} (${model})`, async () => {
|
|
33418
|
+
for (let i = 0; i < count; i++) {
|
|
33419
|
+
const result = await client.generate({
|
|
33420
|
+
prompt: args.prompt,
|
|
33421
|
+
images: refInputs.length > 0 ? refInputs : void 0,
|
|
33422
|
+
model: args.model,
|
|
33423
|
+
aspectRatio: args.aspect_ratio,
|
|
33424
|
+
imageSize: args.image_size,
|
|
33425
|
+
seed: seed + i,
|
|
33426
|
+
thinkingLevel: args.thinking_level,
|
|
33427
|
+
googleSearch: args.google_search,
|
|
33428
|
+
videoUrl: video.videoUrl,
|
|
33429
|
+
videoMimeType: video.videoMimeType,
|
|
33430
|
+
timeoutMs: args.timeout_ms
|
|
33431
|
+
});
|
|
33432
|
+
const { images: [img], text } = result;
|
|
33433
|
+
if (text && !capturedText) capturedText = text;
|
|
33434
|
+
if (result.grounding && !capturedGrounding) capturedGrounding = result.grounding;
|
|
33435
|
+
named.push({ image: img, base: count === 1 ? slug : `${slug}-${String(i + 1).padStart(2, "0")}` });
|
|
33436
|
+
}
|
|
33072
33437
|
});
|
|
33073
|
-
const
|
|
33074
|
-
if (
|
|
33075
|
-
if (
|
|
33076
|
-
|
|
33077
|
-
|
|
33078
|
-
|
|
33079
|
-
|
|
33080
|
-
|
|
33081
|
-
|
|
33082
|
-
meta3.hint = REFINE_HINT;
|
|
33083
|
-
return emit(named, args, meta3);
|
|
33438
|
+
const meta3 = buildMeta(model, seed, args);
|
|
33439
|
+
if (capturedText) meta3.text = capturedText;
|
|
33440
|
+
if (capturedGrounding) meta3.grounding = capturedGrounding;
|
|
33441
|
+
if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
|
|
33442
|
+
meta3.hint = REFINE_HINT;
|
|
33443
|
+
const risk = timeoutRiskHint({ model, imageSize: args.image_size, count });
|
|
33444
|
+
if (risk) meta3.timeout_risk = risk;
|
|
33445
|
+
return emit(named, args, meta3);
|
|
33446
|
+
});
|
|
33084
33447
|
}
|
|
33085
33448
|
);
|
|
33086
33449
|
server.registerTool(
|
|
@@ -33093,35 +33456,54 @@ function registerGenerateTools(server) {
|
|
|
33093
33456
|
images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to input image file(s) (1 = edit, 2+ = compose)"),
|
|
33094
33457
|
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Input images as base64 strings or data URIs"),
|
|
33095
33458
|
filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
|
|
33459
|
+
confirm: schemaConfirm,
|
|
33096
33460
|
...sharedImageSchema
|
|
33097
33461
|
}
|
|
33098
33462
|
},
|
|
33099
|
-
async (args) => {
|
|
33463
|
+
async (args, extra) => {
|
|
33100
33464
|
const hasPaths = (args.images?.length ?? 0) > 0;
|
|
33101
33465
|
const hasBase64 = (args.images_base64?.length ?? 0) > 0;
|
|
33102
33466
|
if (!hasPaths && !hasBase64 && !args.from_clipboard) {
|
|
33103
33467
|
throw new McpToolError("Provide at least one input image via `images`, `images_base64`, or `from_clipboard`.");
|
|
33104
33468
|
}
|
|
33105
|
-
const
|
|
33469
|
+
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images);
|
|
33470
|
+
if (gate) return gate;
|
|
33106
33471
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33107
|
-
const
|
|
33108
|
-
|
|
33109
|
-
const result = await client.generate({
|
|
33472
|
+
const fingerprint = fingerprintRequest("gemini_edit_image", {
|
|
33473
|
+
model,
|
|
33110
33474
|
prompt: args.prompt,
|
|
33111
|
-
|
|
33112
|
-
|
|
33113
|
-
|
|
33114
|
-
|
|
33115
|
-
|
|
33116
|
-
|
|
33117
|
-
|
|
33475
|
+
aspect_ratio: args.aspect_ratio,
|
|
33476
|
+
image_size: args.image_size,
|
|
33477
|
+
thinking_level: args.thinking_level,
|
|
33478
|
+
google_search: args.google_search,
|
|
33479
|
+
images: args.images,
|
|
33480
|
+
images_base64: args.images_base64,
|
|
33481
|
+
from_clipboard: args.from_clipboard
|
|
33482
|
+
});
|
|
33483
|
+
return dispatch({ toolName: "gemini_edit_image", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33484
|
+
const seed = pickSeed(args.seed);
|
|
33485
|
+
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33486
|
+
const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
|
|
33487
|
+
const result = await withProgressHeartbeat(extra, `Editing image (${model})`, () => client.generate({
|
|
33488
|
+
prompt: args.prompt,
|
|
33489
|
+
images: inputs,
|
|
33490
|
+
model: args.model,
|
|
33491
|
+
aspectRatio: args.aspect_ratio,
|
|
33492
|
+
imageSize: args.image_size,
|
|
33493
|
+
seed,
|
|
33494
|
+
thinkingLevel: args.thinking_level,
|
|
33495
|
+
googleSearch: args.google_search,
|
|
33496
|
+
timeoutMs: args.timeout_ms
|
|
33497
|
+
}));
|
|
33498
|
+
const { images: [img], text } = result;
|
|
33499
|
+
const meta3 = buildMeta(model, seed, args);
|
|
33500
|
+
if (text) meta3.text = text;
|
|
33501
|
+
if (result.grounding) meta3.grounding = result.grounding;
|
|
33502
|
+
meta3.hint = REFINE_HINT;
|
|
33503
|
+
const risk = timeoutRiskHint({ model, imageSize: args.image_size });
|
|
33504
|
+
if (risk) meta3.timeout_risk = risk;
|
|
33505
|
+
return emit([{ image: img, base: slug }], args, meta3);
|
|
33118
33506
|
});
|
|
33119
|
-
const { images: [img], text } = result;
|
|
33120
|
-
const meta3 = buildMeta(model, seed, args);
|
|
33121
|
-
if (text) meta3.text = text;
|
|
33122
|
-
if (result.grounding) meta3.grounding = result.grounding;
|
|
33123
|
-
meta3.hint = REFINE_HINT;
|
|
33124
|
-
return emit([{ image: img, base: slug }], args, meta3);
|
|
33125
33507
|
}
|
|
33126
33508
|
);
|
|
33127
33509
|
}
|
|
@@ -33143,45 +33525,71 @@ function registerSetTools(server) {
|
|
|
33143
33525
|
basename: external_exports.string().optional().describe("Base filename prefix for output images (default: slugified master_prompt)"),
|
|
33144
33526
|
master_images: external_exports.array(external_exports.string().min(1)).optional().describe("Reference image paths passed to the master generation call"),
|
|
33145
33527
|
master_images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs for master generation"),
|
|
33528
|
+
confirm: schemaConfirm,
|
|
33146
33529
|
...sharedImageSchema
|
|
33147
33530
|
}
|
|
33148
33531
|
},
|
|
33149
|
-
async (args) => {
|
|
33532
|
+
async (args, extra) => {
|
|
33150
33533
|
if (args.scenes && args.count || !args.scenes && !args.count) {
|
|
33151
33534
|
throw new McpToolError("Provide exactly one of `scenes` or `count`.");
|
|
33152
33535
|
}
|
|
33153
|
-
const
|
|
33536
|
+
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local master image input(s) to the Gemini API", "/v1beta/models/{model}:generateContent", args.master_images);
|
|
33537
|
+
if (gate) return gate;
|
|
33154
33538
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33155
|
-
const
|
|
33156
|
-
|
|
33157
|
-
|
|
33158
|
-
|
|
33159
|
-
|
|
33160
|
-
|
|
33161
|
-
|
|
33162
|
-
|
|
33539
|
+
const fingerprint = fingerprintRequest("gemini_generate_set", {
|
|
33540
|
+
model,
|
|
33541
|
+
master_prompt: args.master_prompt,
|
|
33542
|
+
scenes: args.scenes,
|
|
33543
|
+
count: args.count,
|
|
33544
|
+
reference_mode: args.reference_mode,
|
|
33545
|
+
aspect_ratio: args.aspect_ratio,
|
|
33546
|
+
image_size: args.image_size,
|
|
33547
|
+
thinking_level: args.thinking_level,
|
|
33548
|
+
google_search: args.google_search,
|
|
33549
|
+
master_images: args.master_images,
|
|
33550
|
+
master_images_base64: args.master_images_base64,
|
|
33551
|
+
from_clipboard: args.from_clipboard
|
|
33552
|
+
});
|
|
33553
|
+
return dispatch({ toolName: "gemini_generate_set", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33554
|
+
const seed = pickSeed(args.seed);
|
|
33555
|
+
const cfg = { model: args.model, aspectRatio: args.aspect_ratio, imageSize: args.image_size, thinkingLevel: args.thinking_level, googleSearch: args.google_search, timeoutMs: args.timeout_ms };
|
|
33556
|
+
const slug = args.basename ? baseName(args.basename) : slugify2(args.master_prompt);
|
|
33557
|
+
const masterRefInputs = await gatherImageInputs({ images: args.master_images, images_base64: args.master_images_base64, from_clipboard: args.from_clipboard });
|
|
33558
|
+
const named = [];
|
|
33559
|
+
const masterResult = await withProgressHeartbeat(extra, `Generating image set (${model})`, async () => {
|
|
33560
|
+
const result = await client.generate({
|
|
33561
|
+
prompt: args.master_prompt,
|
|
33562
|
+
images: masterRefInputs.length > 0 ? masterRefInputs : void 0,
|
|
33563
|
+
seed,
|
|
33564
|
+
...cfg
|
|
33565
|
+
});
|
|
33566
|
+
const master = result.images[0];
|
|
33567
|
+
named.push({ image: master, base: `${slug}-master` });
|
|
33568
|
+
const scenePrompts = args.scenes ?? Array.from({ length: args.count ?? 0 }, () => args.master_prompt);
|
|
33569
|
+
const mode = args.reference_mode ?? "master";
|
|
33570
|
+
if (mode === "chain") {
|
|
33571
|
+
let ref = master;
|
|
33572
|
+
for (let i = 0; i < scenePrompts.length; i++) {
|
|
33573
|
+
const { images: [img] } = await client.generate({ prompt: scenePrompts[i], images: [ref], seed: seed + i + 1, ...cfg });
|
|
33574
|
+
named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` });
|
|
33575
|
+
ref = img;
|
|
33576
|
+
}
|
|
33577
|
+
} else {
|
|
33578
|
+
const scenes = await Promise.all(
|
|
33579
|
+
scenePrompts.map((p, i) => client.generate({ prompt: p, images: [master], seed: seed + i + 1, ...cfg }).then((r) => r.images[0]))
|
|
33580
|
+
);
|
|
33581
|
+
scenes.forEach((img, i) => named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` }));
|
|
33582
|
+
}
|
|
33583
|
+
return result;
|
|
33584
|
+
});
|
|
33585
|
+
const masterText = masterResult.text;
|
|
33586
|
+
const meta3 = buildMeta(model, seed, args);
|
|
33587
|
+
if (masterText) meta3.text = masterText;
|
|
33588
|
+
if (masterResult.grounding) meta3.grounding = masterResult.grounding;
|
|
33589
|
+
const risk = timeoutRiskHint({ model, imageSize: args.image_size, count: named.length });
|
|
33590
|
+
if (risk) meta3.timeout_risk = risk;
|
|
33591
|
+
return emit(named, args, meta3);
|
|
33163
33592
|
});
|
|
33164
|
-
const { images: [master], text: masterText } = masterResult;
|
|
33165
|
-
const named = [{ image: master, base: `${slug}-master` }];
|
|
33166
|
-
const scenePrompts = args.scenes ?? Array.from({ length: args.count ?? 0 }, () => args.master_prompt);
|
|
33167
|
-
const mode = args.reference_mode ?? "master";
|
|
33168
|
-
if (mode === "chain") {
|
|
33169
|
-
let ref = master;
|
|
33170
|
-
for (let i = 0; i < scenePrompts.length; i++) {
|
|
33171
|
-
const { images: [img] } = await client.generate({ prompt: scenePrompts[i], images: [ref], seed: seed + i + 1, ...cfg });
|
|
33172
|
-
named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` });
|
|
33173
|
-
ref = img;
|
|
33174
|
-
}
|
|
33175
|
-
} else {
|
|
33176
|
-
const scenes = await Promise.all(
|
|
33177
|
-
scenePrompts.map((p, i) => client.generate({ prompt: p, images: [master], seed: seed + i + 1, ...cfg }).then((r) => r.images[0]))
|
|
33178
|
-
);
|
|
33179
|
-
scenes.forEach((img, i) => named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` }));
|
|
33180
|
-
}
|
|
33181
|
-
const meta3 = buildMeta(model, seed, args);
|
|
33182
|
-
if (masterText) meta3.text = masterText;
|
|
33183
|
-
if (masterResult.grounding) meta3.grounding = masterResult.grounding;
|
|
33184
|
-
return emit(named, args, meta3);
|
|
33185
33593
|
}
|
|
33186
33594
|
);
|
|
33187
33595
|
}
|
|
@@ -33209,7 +33617,7 @@ function registerInteractTools(server) {
|
|
|
33209
33617
|
server.registerTool(
|
|
33210
33618
|
"gemini_interact",
|
|
33211
33619
|
{
|
|
33212
|
-
description: "Preferred tool for iterative or multi-step refinement of a single image \u2014 multi-turn generation/editing via Gemini's Interactions API. To refine, capture the returned interaction `id` and pass it as `previous_interaction_id` on the next call \u2014 do NOT start a new interaction or re-upload the image for each tweak. `continue_last: true` chains from this session's most recent interaction without threading the id. Output is JPEG.",
|
|
33620
|
+
description: "Preferred tool for iterative or multi-step refinement of a single image \u2014 multi-turn generation/editing via Gemini's Interactions API. To refine, capture the returned interaction `id` and pass it as `previous_interaction_id` on the next call \u2014 do NOT start a new interaction or re-upload the image for each tweak. `continue_last: true` chains from this session's most recent interaction without threading the id. If a call times out on the client side, the generation usually still completes: the image plus a `<image>.json` sidecar recording its interaction id land in the output dir, and `continue_last: true` still resumes that interaction \u2014 check the output dir before re-issuing (a re-issue is a second billable generation). Output is JPEG.",
|
|
33213
33621
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
33214
33622
|
inputSchema: {
|
|
33215
33623
|
input: external_exports.string().min(1).describe("Text prompt or editing instruction"),
|
|
@@ -33230,10 +33638,14 @@ function registerInteractTools(server) {
|
|
|
33230
33638
|
),
|
|
33231
33639
|
video_url: external_exports.string().url().optional().describe("Public YouTube URL (or a previously uploaded Files API uri) as a video reference (video\u2192image; use a Flash model e.g. gemini-3.1-flash-image)"),
|
|
33232
33640
|
video_path: videoPathSchema,
|
|
33233
|
-
|
|
33641
|
+
timeout_ms: timeoutMsSchema,
|
|
33642
|
+
idempotency_key: idempotencyKeySchema,
|
|
33643
|
+
async: asyncSchema,
|
|
33644
|
+
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)"),
|
|
33645
|
+
confirm: schemaConfirm
|
|
33234
33646
|
}
|
|
33235
33647
|
},
|
|
33236
|
-
async (args) => {
|
|
33648
|
+
async (args, extra) => {
|
|
33237
33649
|
let previousInteractionId = args.previous_interaction_id;
|
|
33238
33650
|
if (!previousInteractionId && args.continue_last) {
|
|
33239
33651
|
if (!lastInteractionId) {
|
|
@@ -33248,46 +33660,89 @@ function registerInteractTools(server) {
|
|
|
33248
33660
|
if (previousInteractionId && images?.length) {
|
|
33249
33661
|
({ kept: images, dropped } = splitReattachedOutputs(images));
|
|
33250
33662
|
}
|
|
33251
|
-
const
|
|
33252
|
-
|
|
33253
|
-
const
|
|
33663
|
+
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local file input(s) to the Gemini Interactions API", "/v1beta/interactions", images, args.video_path);
|
|
33664
|
+
if (gate) return gate;
|
|
33665
|
+
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33666
|
+
const fingerprint = fingerprintRequest("gemini_interact", {
|
|
33667
|
+
model,
|
|
33254
33668
|
input: args.input,
|
|
33255
|
-
|
|
33256
|
-
|
|
33257
|
-
|
|
33258
|
-
|
|
33259
|
-
|
|
33260
|
-
|
|
33261
|
-
|
|
33262
|
-
|
|
33263
|
-
|
|
33264
|
-
|
|
33669
|
+
aspect_ratio: args.aspect_ratio,
|
|
33670
|
+
image_size: args.image_size,
|
|
33671
|
+
thinking_level: args.thinking_level,
|
|
33672
|
+
previous_interaction_id: previousInteractionId,
|
|
33673
|
+
google_search: args.google_search,
|
|
33674
|
+
search_types: args.search_types,
|
|
33675
|
+
images,
|
|
33676
|
+
images_base64: args.images_base64,
|
|
33677
|
+
from_clipboard: args.from_clipboard,
|
|
33678
|
+
video_url: args.video_url,
|
|
33679
|
+
video_path: args.video_path
|
|
33265
33680
|
});
|
|
33266
|
-
|
|
33267
|
-
|
|
33268
|
-
|
|
33269
|
-
|
|
33270
|
-
|
|
33271
|
-
|
|
33272
|
-
|
|
33273
|
-
|
|
33274
|
-
|
|
33275
|
-
|
|
33276
|
-
|
|
33277
|
-
|
|
33278
|
-
|
|
33279
|
-
|
|
33280
|
-
|
|
33281
|
-
|
|
33681
|
+
return dispatch({ toolName: "gemini_interact", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33682
|
+
const inputs = await gatherImageInputs({ images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
|
|
33683
|
+
const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args));
|
|
33684
|
+
const r = await withProgressHeartbeat(extra, "Generating image (Gemini Interactions API)", () => client.interact({
|
|
33685
|
+
input: args.input,
|
|
33686
|
+
images: inputs.length ? inputs : void 0,
|
|
33687
|
+
model: args.model,
|
|
33688
|
+
aspectRatio: args.aspect_ratio,
|
|
33689
|
+
imageSize: args.image_size,
|
|
33690
|
+
thinkingLevel: args.thinking_level,
|
|
33691
|
+
previousInteractionId,
|
|
33692
|
+
googleSearch: args.google_search,
|
|
33693
|
+
searchTypes: args.search_types,
|
|
33694
|
+
videoUrl: video.videoUrl,
|
|
33695
|
+
videoMimeType: video.videoMimeType,
|
|
33696
|
+
timeoutMs: args.timeout_ms
|
|
33697
|
+
}));
|
|
33698
|
+
lastInteractionId = r.id;
|
|
33699
|
+
const meta3 = { model, interaction_id: r.id };
|
|
33700
|
+
if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
33701
|
+
if (dropped.length) meta3.dropped_previous_output = dropped;
|
|
33702
|
+
meta3.hint = `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true) \u2014 do NOT re-attach the returned image as an input; the interaction already contains it.`;
|
|
33703
|
+
if (r.text) meta3.text = r.text;
|
|
33704
|
+
if (r.grounding) meta3.grounding = r.grounding;
|
|
33705
|
+
if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
|
|
33706
|
+
const risk = timeoutRiskHint({ model, imageSize: args.image_size });
|
|
33707
|
+
if (risk) meta3.timeout_risk = risk;
|
|
33708
|
+
const slug = args.filename ? baseName(args.filename) : slugify2(args.input);
|
|
33709
|
+
const named = r.images.map((image, i) => ({
|
|
33710
|
+
image,
|
|
33711
|
+
base: r.images.length > 1 ? `${slug}-${String(i + 1).padStart(2, "0")}` : slug
|
|
33712
|
+
}));
|
|
33713
|
+
return emit(named, args, meta3, async (paths) => {
|
|
33714
|
+
for (const p of paths) writtenOutputs.add(p);
|
|
33715
|
+
try {
|
|
33716
|
+
for (const p of paths) await writeSidecar(p, { ...meta3, images: paths });
|
|
33717
|
+
} catch (err) {
|
|
33718
|
+
meta3.sidecar_error = err instanceof Error ? err.message : String(err);
|
|
33719
|
+
}
|
|
33720
|
+
});
|
|
33282
33721
|
});
|
|
33283
33722
|
}
|
|
33284
33723
|
);
|
|
33285
33724
|
}
|
|
33286
33725
|
|
|
33726
|
+
// src/tools/jobs.ts
|
|
33727
|
+
init_zod();
|
|
33728
|
+
function registerJobTools(server) {
|
|
33729
|
+
server.registerTool(
|
|
33730
|
+
"gemini_get_result",
|
|
33731
|
+
{
|
|
33732
|
+
description: 'Retrieve an async generation started with `async: true`. Pass the returned `job_id`: while running it reports status "running"; on completion it returns the normal result (image paths / inline images + meta); on failure it raises the recorded error. Jobs are per-process and expire ~10 min after completion \u2014 if a job id is unknown, check the output dir / <image>.json sidecar.',
|
|
33733
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
33734
|
+
inputSchema: {
|
|
33735
|
+
job_id: external_exports.string().min(1).describe("The job_id returned by a generation tool called with async: true")
|
|
33736
|
+
}
|
|
33737
|
+
},
|
|
33738
|
+
async (args) => getJobResult(args.job_id)
|
|
33739
|
+
);
|
|
33740
|
+
}
|
|
33741
|
+
|
|
33287
33742
|
// src/index.ts
|
|
33288
33743
|
await runMcp({
|
|
33289
33744
|
name: "gemini-mcp",
|
|
33290
33745
|
version: VERSION,
|
|
33291
33746
|
banner: "[gemini-mcp] This project was developed and is maintained by AI (Claude). Use at your own discretion.",
|
|
33292
|
-
tools: [registerModelTools, registerGenerateTools, registerSetTools, registerInteractTools]
|
|
33747
|
+
tools: [registerModelTools, registerGenerateTools, registerSetTools, registerInteractTools, registerJobTools]
|
|
33293
33748
|
});
|