@chrischall/gemini-mcp 2.3.2 → 2.3.4

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/dist/bundle.js CHANGED
@@ -4988,7 +4988,7 @@ var require_memoizer = __commonJS({
4988
4988
  var NONE2 = 0;
4989
4989
  var ASSUMED2 = 1;
4990
4990
  var PROVEN2 = 2;
4991
- function isRecursive2(inst, stack, resolve5) {
4991
+ function isRecursive2(inst, stack, resolve6) {
4992
4992
  const cached2 = recursive2.get(inst);
4993
4993
  if (cached2 !== void 0)
4994
4994
  return cached2 ? PROVEN2 : NONE2;
@@ -4998,7 +4998,7 @@ var require_memoizer = __commonJS({
4998
4998
  let result = NONE2;
4999
4999
  const check2 = (child) => {
5000
5000
  if (result !== PROVEN2 && child?._zod) {
5001
- const answer = isRecursive2(child, stack, resolve5);
5001
+ const answer = isRecursive2(child, stack, resolve6);
5002
5002
  if (answer > result)
5003
5003
  result = answer;
5004
5004
  }
@@ -5009,7 +5009,7 @@ var require_memoizer = __commonJS({
5009
5009
  const desc = Object.getOwnPropertyDescriptor(sh, key);
5010
5010
  if (spread && !desc.enumerable)
5011
5011
  continue;
5012
- const child = desc.get ? ASSUMED2 : desc.value?._zod ? isRecursive2(desc.value, stack, resolve5) : NONE2;
5012
+ const child = desc.get ? ASSUMED2 : desc.value?._zod ? isRecursive2(desc.value, stack, resolve6) : NONE2;
5013
5013
  if (child > answer)
5014
5014
  answer = child;
5015
5015
  }
@@ -5073,7 +5073,7 @@ var require_memoizer = __commonJS({
5073
5073
  break;
5074
5074
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5075
5075
  case "lazy": {
5076
- const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
5076
+ const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
5077
5077
  merge2(inner ? isRecursive2(inner, stack, false) : ASSUMED2);
5078
5078
  break;
5079
5079
  }
@@ -26945,14 +26945,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
26945
26945
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
26946
26946
  }
26947
26947
  function sleep(ms, signal) {
26948
- return new Promise((resolve5, reject) => {
26948
+ return new Promise((resolve6, reject) => {
26949
26949
  if (signal?.aborted) {
26950
26950
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
26951
26951
  return;
26952
26952
  }
26953
26953
  const timer = setTimeout(() => {
26954
26954
  signal?.removeEventListener("abort", onAbort);
26955
- resolve5();
26955
+ resolve6();
26956
26956
  }, ms);
26957
26957
  const onAbort = () => {
26958
26958
  clearTimeout(timer);
@@ -28770,7 +28770,7 @@ var init_src_Cqbh3MYc = __esm({
28770
28770
  const flowStartedAt = Date.now();
28771
28771
  let onAbort;
28772
28772
  let cleanupMessageId;
28773
- return new Promise((resolve5, reject) => {
28773
+ return new Promise((resolve6, reject) => {
28774
28774
  const earlyReject = (error62) => {
28775
28775
  reject(error62);
28776
28776
  };
@@ -28839,7 +28839,7 @@ var init_src_Cqbh3MYc = __esm({
28839
28839
  }
28840
28840
  if (decoded.kind === "invalid") return reject(decoded.error);
28841
28841
  if (decoded.kind === "input_required") {
28842
- if (options?.allowInputRequired === true) return resolve5(manualInputRequiredValue(decoded));
28842
+ if (options?.allowInputRequired === true) return resolve6(manualInputRequiredValue(decoded));
28843
28843
  const flow = {
28844
28844
  codec: codec2,
28845
28845
  request,
@@ -28851,11 +28851,11 @@ var init_src_Cqbh3MYc = __esm({
28851
28851
  params
28852
28852
  }, resultSchema, legOptions)
28853
28853
  };
28854
- return resolve5(this._resolveNonCompleteResult(decoded, flow));
28854
+ return resolve6(this._resolveNonCompleteResult(decoded, flow));
28855
28855
  }
28856
28856
  const result = decoded.result;
28857
28857
  validateStandardSchema(resultSchema, result).then((parseResult) => {
28858
- if (parseResult.success) resolve5(parseResult.data);
28858
+ if (parseResult.success) resolve6(parseResult.data);
28859
28859
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
28860
28860
  }, reject);
28861
28861
  });
@@ -31674,7 +31674,7 @@ var init_ajvProvider_CEoC_sr = __esm({
31674
31674
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
31675
31675
  const schOrFunc = root.refs[ref];
31676
31676
  if (schOrFunc) return schOrFunc;
31677
- let _sch = resolve5.call(this, root, ref);
31677
+ let _sch = resolve6.call(this, root, ref);
31678
31678
  if (_sch === void 0) {
31679
31679
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
31680
31680
  const { schemaId } = this.opts;
@@ -31700,7 +31700,7 @@ var init_ajvProvider_CEoC_sr = __esm({
31700
31700
  function sameSchemaEnv(s1, s2) {
31701
31701
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
31702
31702
  }
31703
- function resolve5(root, ref) {
31703
+ function resolve6(root, ref) {
31704
31704
  let sch;
31705
31705
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
31706
31706
  return sch || this.schemas[ref] || resolveSchema.call(this, root, ref);
@@ -32150,7 +32150,7 @@ var init_ajvProvider_CEoC_sr = __esm({
32150
32150
  else if (typeof uri === "object") uri = parse3(serialize(uri, options), options);
32151
32151
  return uri;
32152
32152
  }
32153
- function resolve5(baseURI, relativeURI, options) {
32153
+ function resolve6(baseURI, relativeURI, options) {
32154
32154
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
32155
32155
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
32156
32156
  schemelessOptions.skipEscape = true;
@@ -32324,7 +32324,7 @@ var init_ajvProvider_CEoC_sr = __esm({
32324
32324
  const fastUri = {
32325
32325
  SCHEMES,
32326
32326
  normalize,
32327
- resolve: resolve5,
32327
+ resolve: resolve6,
32328
32328
  resolveComponent,
32329
32329
  equal,
32330
32330
  serialize,
@@ -38107,7 +38107,7 @@ var init_stdio = __esm({
38107
38107
  }
38108
38108
  send(message) {
38109
38109
  if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
38110
- return new Promise((resolve5, reject) => {
38110
+ return new Promise((resolve6, reject) => {
38111
38111
  const json2 = serializeMessage(message);
38112
38112
  let settled = false;
38113
38113
  const onError = (error62) => {
@@ -38122,14 +38122,14 @@ var init_stdio = __esm({
38122
38122
  settled = true;
38123
38123
  this._stdout.off("error", onError);
38124
38124
  this._stdout.off("drain", onDrain);
38125
- resolve5();
38125
+ resolve6();
38126
38126
  };
38127
38127
  this._stdout.once("error", onError);
38128
38128
  if (this._stdout.write(json2)) {
38129
38129
  if (settled) return;
38130
38130
  settled = true;
38131
38131
  this._stdout.off("error", onError);
38132
- resolve5();
38132
+ resolve6();
38133
38133
  } else if (!settled) this._stdout.once("drain", onDrain);
38134
38134
  });
38135
38135
  }
@@ -38183,14 +38183,14 @@ var init_stdio = __esm({
38183
38183
  */
38184
38184
  async whenRequestsAnswered(timeoutMs) {
38185
38185
  if (this._closed || this._pendingRequests.size === 0) return true;
38186
- return await new Promise((resolve5) => {
38186
+ return await new Promise((resolve6) => {
38187
38187
  const waiter = () => {
38188
38188
  clearTimeout(timer);
38189
- resolve5(true);
38189
+ resolve6(true);
38190
38190
  };
38191
38191
  const timer = setTimeout(() => {
38192
38192
  this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
38193
- resolve5(false);
38193
+ resolve6(false);
38194
38194
  }, timeoutMs);
38195
38195
  this._drainWaiters.push(waiter);
38196
38196
  });
@@ -38231,6 +38231,30 @@ var init_dist = __esm({
38231
38231
  });
38232
38232
 
38233
38233
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
38234
+ function firstDeclared(err, read) {
38235
+ let cur = err;
38236
+ for (let depth = 0; depth < DECLARATION_CAUSE_DEPTH && cur !== null && typeof cur === "object"; depth++) {
38237
+ const found = read(cur);
38238
+ if (found !== void 0)
38239
+ return found;
38240
+ cur = cur.cause;
38241
+ }
38242
+ return void 0;
38243
+ }
38244
+ function errorStatusOf(err) {
38245
+ return firstDeclared(err, (link) => {
38246
+ const e = link;
38247
+ if (typeof e.status === "number")
38248
+ return e.status;
38249
+ return typeof e.statusCode === "number" ? e.statusCode : void 0;
38250
+ });
38251
+ }
38252
+ function errorKindOf(err) {
38253
+ return firstDeclared(err, (link) => {
38254
+ const k = link.kind;
38255
+ return typeof k === "string" && MCP_TOOL_ERROR_KINDS.has(k) ? k : void 0;
38256
+ });
38257
+ }
38234
38258
  function redactJsonCookie(quote) {
38235
38259
  const stringRe = quote === '"' ? DQ_STRING_RE : SQ_STRING_RE;
38236
38260
  return (_m, prefix, key, value) => {
@@ -38258,21 +38282,82 @@ function messageOf(err) {
38258
38282
  return err.message;
38259
38283
  return String(err);
38260
38284
  }
38261
- var DEFAULT_ERROR_MESSAGE_MAX, McpToolError, HEADER_NAME_RUN, BEARER_RE, JWT_RE, BASIC_AUTH_RE, SET_COOKIE_RE, COOKIE_HEADER_RE, API_KEY_RE, GOOGLE_OAUTH_TOKEN_RE, QUERY_SECRET_RE, OAUTH_CODE_RE, FORM_PASSWORD_RE, HEADER_SECRET_RE, AWS_SIGV4_RE, JSON_SECRET_KEYS, JSON_SECRET_DQ_RE, JSON_SECRET_SQ_RE, JSON_SECRET_NUM_RE, JSON_COOKIE_DQ_RE, JSON_COOKIE_SQ_RE, DQ_STRING_RE, SQ_STRING_RE, ERROR_REDACTION_INPUT_MAX;
38285
+ var DEFAULT_ERROR_MESSAGE_MAX, MCP_TOOL_ERROR_KINDS, McpToolError, DECLARATION_CAUSE_DEPTH, UPSTREAM_BODY_PHRASE, UpstreamFormatError, HEADER_NAME_RUN, BEARER_RE, JWT_RE, BASIC_AUTH_RE, SET_COOKIE_RE, COOKIE_HEADER_RE, API_KEY_RE, GOOGLE_OAUTH_TOKEN_RE, QUERY_SECRET_RE, OAUTH_CODE_RE, FORM_PASSWORD_RE, HEADER_SECRET_RE, AWS_SIGV4_RE, JSON_SECRET_KEYS, JSON_SECRET_DQ_RE, JSON_SECRET_SQ_RE, JSON_SECRET_NUM_RE, JSON_COOKIE_DQ_RE, JSON_COOKIE_SQ_RE, DQ_STRING_RE, SQ_STRING_RE, ERROR_REDACTION_INPUT_MAX;
38262
38286
  var init_errors = __esm({
38263
38287
  "node_modules/@chrischall/mcp-utils/dist/errors/index.js"() {
38264
38288
  DEFAULT_ERROR_MESSAGE_MAX = 500;
38289
+ MCP_TOOL_ERROR_KINDS = /* @__PURE__ */ new Set([
38290
+ "no_credential",
38291
+ "credential_rejected",
38292
+ "edge_blocked",
38293
+ "session_expired",
38294
+ "verification_pending",
38295
+ "timeout",
38296
+ "http",
38297
+ "transport",
38298
+ "too_large",
38299
+ "unknown"
38300
+ ]);
38265
38301
  McpToolError = class extends Error {
38266
38302
  /** Actionable remediation text, when one applies. */
38267
38303
  hint;
38304
+ /** The upstream HTTP status this failure came from, when known. */
38305
+ status;
38306
+ /** What kind of failure this is, when the thrower knows. */
38307
+ kind;
38268
38308
  constructor(message, opts) {
38269
38309
  super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
38270
38310
  this.name = "McpToolError";
38271
38311
  if (opts?.hint !== void 0)
38272
38312
  this.hint = opts.hint;
38313
+ if (opts?.status !== void 0)
38314
+ this.status = opts.status;
38315
+ if (opts?.kind !== void 0)
38316
+ this.kind = opts.kind;
38273
38317
  Object.setPrototypeOf(this, new.target.prototype);
38274
38318
  }
38275
38319
  };
38320
+ DECLARATION_CAUSE_DEPTH = 4;
38321
+ UPSTREAM_BODY_PHRASE = {
38322
+ "non-json": "a non-JSON response",
38323
+ empty: "an empty body",
38324
+ null: "null",
38325
+ array: "an array",
38326
+ object: "an object",
38327
+ string: "a string",
38328
+ number: "a number",
38329
+ boolean: "a boolean"
38330
+ };
38331
+ UpstreamFormatError = class extends McpToolError {
38332
+ /** What the body turned out to be. */
38333
+ received;
38334
+ /** The shape the caller required, when it required one. */
38335
+ expected;
38336
+ /** The response `Content-Type`, when known. (`status` is the base class's.) */
38337
+ contentType;
38338
+ constructor(details) {
38339
+ const service = details.service ?? "The upstream service";
38340
+ const request = details.method !== void 0 && details.path !== void 0 ? `${details.method.toUpperCase()} ${details.path}` : void 0;
38341
+ const meta3 = [
38342
+ details.status !== void 0 ? `HTTP ${details.status}` : void 0,
38343
+ details.contentType || void 0
38344
+ ].filter((p) => p !== void 0);
38345
+ const what = details.expected !== void 0 ? `${UPSTREAM_BODY_PHRASE[details.received]} where a JSON ${details.expected} was expected` + (request ? ` for ${request}` : "") : `${UPSTREAM_BODY_PHRASE[details.received]}` + (request ? ` to ${request}` : "");
38346
+ const message = `${service} returned ${what}${meta3.length > 0 ? ` (${meta3.join(", ")})` : ""}.`;
38347
+ const hint = details.received === "non-json" ? "The service answered with something other than JSON, usually a sign-in page or interstitial, or its API has changed. Check the session, then retry." : "The service answered with a body of the wrong shape: the resource may be missing or its API may have changed.";
38348
+ super(truncateErrorMessage(message), {
38349
+ hint,
38350
+ cause: details.cause,
38351
+ ...details.status !== void 0 ? { status: details.status } : {}
38352
+ });
38353
+ this.name = "UpstreamFormatError";
38354
+ this.received = details.received;
38355
+ if (details.expected !== void 0)
38356
+ this.expected = details.expected;
38357
+ if (details.contentType)
38358
+ this.contentType = details.contentType;
38359
+ }
38360
+ };
38276
38361
  HEADER_NAME_RUN = "[a-z0-9_-]{0,40}";
38277
38362
  BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
38278
38363
  JWT_RE = /(?<![A-Za-z0-9_-])[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}(?![A-Za-z0-9_-])/g;
@@ -38294,7 +38379,7 @@ var init_errors = __esm({
38294
38379
  // webhook signing secret (Stripe-style)
38295
38380
  ].map((p) => `\\b${p}`).join("|"), "g");
38296
38381
  GOOGLE_OAUTH_TOKEN_RE = /(?<![A-Za-z0-9+/])(?:ya29\.|1\/\/)[A-Za-z0-9._-]{8,}/g;
38297
- QUERY_SECRET_RE = new RegExp(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
38382
+ QUERY_SECRET_RE = new RegExp(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:code[_-]?)?verifier|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
38298
38383
  OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
38299
38384
  FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
38300
38385
  HEADER_SECRET_RE = new RegExp(`(\\b(?:x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN}|api[-_]?key)\\s*:\\s*)[^\\s,;"'<>\`]+`, "gi");
@@ -38305,6 +38390,8 @@ var init_errors = __esm({
38305
38390
  "api[_-]?secret",
38306
38391
  "(?:x[_-])?api[_-]?key",
38307
38392
  "private[_-]?key",
38393
+ "code[_-]?verifier",
38394
+ // PKCE; a bare `verifier` key is too generic for JSON
38308
38395
  `x[_-]${HEADER_NAME_RUN}?(?:api[_-]?key|token|secret|auth)${HEADER_NAME_RUN}`,
38309
38396
  // bounded like HEADER_SECRET_RE
38310
38397
  "(?:proxy-)?authorization",
@@ -42820,7 +42907,7 @@ function isRef(value) {
42820
42907
  function cloneIssues(issues) {
42821
42908
  return issues.map((iss) => iss.path ? { ...iss, path: iss.path.slice() } : { ...iss });
42822
42909
  }
42823
- function isRecursive(inst, stack, resolve5) {
42910
+ function isRecursive(inst, stack, resolve6) {
42824
42911
  const cached2 = recursive.get(inst);
42825
42912
  if (cached2 !== void 0)
42826
42913
  return cached2 ? PROVEN : NONE;
@@ -42830,7 +42917,7 @@ function isRecursive(inst, stack, resolve5) {
42830
42917
  let result = NONE;
42831
42918
  const check2 = (child) => {
42832
42919
  if (result !== PROVEN && child?._zod) {
42833
- const answer = isRecursive(child, stack, resolve5);
42920
+ const answer = isRecursive(child, stack, resolve6);
42834
42921
  if (answer > result)
42835
42922
  result = answer;
42836
42923
  }
@@ -42841,7 +42928,7 @@ function isRecursive(inst, stack, resolve5) {
42841
42928
  const desc = Object.getOwnPropertyDescriptor(sh, key);
42842
42929
  if (spread && !desc.enumerable)
42843
42930
  continue;
42844
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
42931
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
42845
42932
  if (child > answer)
42846
42933
  answer = child;
42847
42934
  }
@@ -42905,7 +42992,7 @@ function isRecursive(inst, stack, resolve5) {
42905
42992
  break;
42906
42993
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
42907
42994
  case "lazy": {
42908
- const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
42995
+ const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
42909
42996
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
42910
42997
  break;
42911
42998
  }
@@ -58852,6 +58939,15 @@ ${JSON.stringify(preview, null, 2)}`,
58852
58939
  };
58853
58940
  const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
58854
58941
  const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
58942
+ if (options.elicitation === false) {
58943
+ return textResult({
58944
+ confirmed: false,
58945
+ dispatched: false,
58946
+ action: options.action,
58947
+ reason: "confirmation-unsupported",
58948
+ note: options.unsupportedNote ? `${DISABLED_NOTE}. ${options.unsupportedNote}` : `${DISABLED_NOTE}.`
58949
+ });
58950
+ }
58855
58951
  if (response.kind === "missing") {
58856
58952
  if (callerAcceptsFormElicitation(ctx) === false) {
58857
58953
  return textResult({
@@ -58882,7 +58978,7 @@ ${JSON.stringify(preview, null, 2)}`,
58882
58978
  note: "Nothing was changed because the confirmation was declined, cancelled, or left unchecked."
58883
58979
  });
58884
58980
  }
58885
- var DEFAULT_REQUEST_KEY, DEFAULT_CONFIRMATION_LABEL, UNSUPPORTED_NOTE, STATE_PREFIX;
58981
+ var DEFAULT_REQUEST_KEY, DEFAULT_CONFIRMATION_LABEL, UNSUPPORTED_NOTE, DISABLED_NOTE, STATE_PREFIX;
58886
58982
  var init_confirmation = __esm({
58887
58983
  "node_modules/@chrischall/mcp-utils/dist/server/confirmation.js"() {
58888
58984
  init_dist();
@@ -58893,6 +58989,7 @@ var init_confirmation = __esm({
58893
58989
  DEFAULT_REQUEST_KEY = "confirmation";
58894
58990
  DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
58895
58991
  UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
58992
+ DISABLED_NOTE = "Nothing was done because confirmation prompts are turned off for this server, and this action is never taken without one";
58896
58993
  STATE_PREFIX = "mcpu.confirm.v1.";
58897
58994
  }
58898
58995
  });
@@ -59046,7 +59143,7 @@ async function tokenConfirmation(action, fb) {
59046
59143
  }
59047
59144
  async function requireConfirmationWithFallback(ctx, options) {
59048
59145
  const { tokenFallback, ...confirmation } = options;
59049
- if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
59146
+ if (tokenFallback && (options.elicitation === false || callerAcceptsFormElicitation(ctx) === false)) {
59050
59147
  return tokenConfirmation(options.action, tokenFallback);
59051
59148
  }
59052
59149
  return requireConfirmation(ctx, confirmation);
@@ -59085,7 +59182,7 @@ function readEnvVar(key, opts = {}) {
59085
59182
  if (typeof raw === "string") {
59086
59183
  const trimmed = raw.trim();
59087
59184
  if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
59088
- return trimmed;
59185
+ return opts.trim === false ? raw : trimmed;
59089
59186
  }
59090
59187
  }
59091
59188
  return opts.default;
@@ -59223,6 +59320,20 @@ function readConfirmMode(env = process.env) {
59223
59320
  }
59224
59321
  return "refuse";
59225
59322
  }
59323
+ function readConfirmElicitation(env = process.env) {
59324
+ const raw = readEnvVar("MCP_CONFIRM_ELICITATION", { env })?.trim().toLowerCase();
59325
+ if (!raw || raw === "on")
59326
+ return "on";
59327
+ if (raw === "off")
59328
+ return "off";
59329
+ const key = `elicitation:${raw}`;
59330
+ if (!warned.has(key)) {
59331
+ warned.add(key);
59332
+ process.stderr.write(`MCP_CONFIRM_ELICITATION="${raw}" is not one of on, off; treating it as on.
59333
+ `);
59334
+ }
59335
+ return "on";
59336
+ }
59226
59337
  function readConfirmTtl(env) {
59227
59338
  const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
59228
59339
  if (!raw)
@@ -59272,19 +59383,29 @@ function boundArgs(args) {
59272
59383
  }
59273
59384
  function confirmationFromEnv(options) {
59274
59385
  const { tool, account, confirmToken, subject, instruction, args, env = process.env, spent: callerSpent, ...rest } = options;
59386
+ if (args === void 0 || args === null) {
59387
+ throw new TypeError(`confirmationFromEnv: ${tool} passed no args; pass the tool's validated arguments ({} for a tool with none).`);
59388
+ }
59275
59389
  const spent = callerSpent ?? spentTokenStoreFromEnv(env);
59276
59390
  const mode = readConfirmMode(env);
59277
59391
  const ttlSeconds = readConfirmTtl(env);
59278
- const bound = args === void 0 ? void 0 : boundArgs(args);
59279
- const confirmation = bound === void 0 || rest.binding ? rest : { ...rest, binding: { key: confirmKeyFromEnv(env), args: bound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 } };
59392
+ const elicitationOff = readConfirmElicitation(env) === "off";
59393
+ const bound = boundArgs(args);
59394
+ const elicitationBound = account === void 0 ? { args: bound } : { account, args: bound };
59395
+ const bindingAdded = rest.binding ? rest : {
59396
+ ...rest,
59397
+ binding: { key: confirmKeyFromEnv(env), args: elicitationBound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 }
59398
+ };
59399
+ const confirmation = elicitationOff ? { ...bindingAdded, elicitation: false } : bindingAdded;
59280
59400
  if (mode === "refuse" || ttlSeconds === void 0) {
59281
- const hint = mode === "refuse" ? REFUSE_HINT : BAD_TTL_HINT;
59401
+ const reason = mode === "refuse" ? REFUSE_HINT : BAD_TTL_HINT;
59402
+ const hint = elicitationOff ? `${ELICITATION_OFF_HINT} ${reason}` : reason;
59282
59403
  return {
59283
59404
  ...confirmation,
59284
59405
  unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${hint}` : hint
59285
59406
  };
59286
59407
  }
59287
- const boundSubject = bound === void 0 ? subject : async () => {
59408
+ const boundSubject = async () => {
59288
59409
  const read = await subject();
59289
59410
  if ("content" in read)
59290
59411
  return read;
@@ -59304,7 +59425,7 @@ function confirmationFromEnv(options) {
59304
59425
  }
59305
59426
  };
59306
59427
  }
59307
- var MODES, DEFAULT_TTL_SECONDS2, warned, processKey, fileStores, CONFIRM_TOKEN_AUTO_INSTRUCTION, REFUSE_HINT, BAD_TTL_HINT;
59428
+ var MODES, DEFAULT_TTL_SECONDS2, warned, processKey, fileStores, CONFIRM_TOKEN_AUTO_INSTRUCTION, REFUSE_HINT, ELICITATION_OFF_HINT, BAD_TTL_HINT;
59308
59429
  var init_confirm_env = __esm({
59309
59430
  "node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js"() {
59310
59431
  init_config();
@@ -59316,6 +59437,7 @@ var init_confirm_env = __esm({
59316
59437
  fileStores = /* @__PURE__ */ new Map();
59317
59438
  CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
59318
59439
  REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
59440
+ ELICITATION_OFF_HINT = "MCP_CONFIRM_ELICITATION=off on the server turns confirmation prompts off.";
59319
59441
  BAD_TTL_HINT = "MCP_CONFIRM_TTL_SECONDS on the server is not a positive whole number of seconds; fix it to allow two-step confirmation.";
59320
59442
  }
59321
59443
  });
@@ -59336,12 +59458,14 @@ var init_merge_write = __esm({
59336
59458
 
59337
59459
  // node_modules/@chrischall/mcp-utils/dist/server/index.js
59338
59460
  function hintResultOrRethrow(err) {
59461
+ if (err instanceof ProtocolError && err.code === ProtocolErrorCode.UrlElicitationRequired)
59462
+ throw err;
59339
59463
  if (err instanceof McpToolError && err.hint) {
59340
59464
  return errorResult(`${err.message}
59341
59465
 
59342
59466
  Hint: ${err.hint}`);
59343
59467
  }
59344
- throw err;
59468
+ return errorResult(messageOf(err));
59345
59469
  }
59346
59470
  function mcpRequestFrom(args) {
59347
59471
  const ctx = args.at(-1);
@@ -59684,10 +59808,12 @@ var init_write = __esm({
59684
59808
 
59685
59809
  // node_modules/@chrischall/mcp-utils/dist/fs/output.js
59686
59810
  import { existsSync as existsSync2, mkdirSync as mkdirSync3 } from "node:fs";
59811
+ import { homedir as homedir2 } from "node:os";
59812
+ import { join as join5 } from "node:path";
59687
59813
  function resolveOutputDir(perCall, envVar, opts = {}) {
59688
59814
  const raw = perCall ?? readEnvVar(envVar, opts.env ? { env: opts.env } : {});
59689
59815
  if (!raw)
59690
- return process.cwd();
59816
+ return defaultOutputDir(envVar, opts.name);
59691
59817
  const dir = expandPath(raw);
59692
59818
  if (perCall !== void 0 && opts.allowedRoots) {
59693
59819
  assertPathWithinRoots(dir, opts.allowedRoots);
@@ -59698,9 +59824,23 @@ function resolveOutputDir(perCall, envVar, opts = {}) {
59698
59824
  mkdirSync3(dir, { recursive: true });
59699
59825
  return dir;
59700
59826
  }
59827
+ function defaultOutputDir(envVar, name) {
59828
+ if (name === void 0) {
59829
+ throw new McpToolError(`No output directory is configured: ${envVar} is not set.`, {
59830
+ hint: `Set ${envVar} to a writable directory, or pass an output directory with the call.`
59831
+ });
59832
+ }
59833
+ if (!name || name === "." || name === ".." || /[/\\]/.test(name)) {
59834
+ throw new TypeError(`resolveOutputDir: name must be a single path segment, got ${JSON.stringify(name)}`);
59835
+ }
59836
+ const dir = join5(homedir2(), "Downloads", name);
59837
+ mkdirSync3(dir, { recursive: true, mode: 448 });
59838
+ return dir;
59839
+ }
59701
59840
  var init_output = __esm({
59702
59841
  "node_modules/@chrischall/mcp-utils/dist/fs/output.js"() {
59703
59842
  init_config();
59843
+ init_errors();
59704
59844
  init_confine();
59705
59845
  init_write();
59706
59846
  }
@@ -59860,6 +60000,7 @@ var init_scrape = __esm({
59860
60000
  // node_modules/@chrischall/mcp-utils/dist/http/throttle.js
59861
60001
  var init_throttle = __esm({
59862
60002
  "node_modules/@chrischall/mcp-utils/dist/http/throttle.js"() {
60003
+ init_cancel();
59863
60004
  }
59864
60005
  });
59865
60006
 
@@ -59885,6 +60026,113 @@ var init_net_atoms = __esm({
59885
60026
  }
59886
60027
  });
59887
60028
 
60029
+ // node_modules/@chrischall/mcp-utils/dist/http/body-limit.js
60030
+ async function readBytesCapped(res, maxBytes, service, bounded) {
60031
+ if (maxBytes !== void 0) {
60032
+ const declared = Number(res.headers.get("content-length") ?? NaN);
60033
+ if (Number.isFinite(declared) && declared > maxBytes) {
60034
+ cancelStream(res.body);
60035
+ throw new ResponseTooLargeError(service, maxBytes);
60036
+ }
60037
+ }
60038
+ if (!res.body)
60039
+ return new Uint8Array(0);
60040
+ const reader = res.body.getReader();
60041
+ const chunks = [];
60042
+ let total = 0;
60043
+ try {
60044
+ for (; ; ) {
60045
+ const { done, value } = await bounded(reader.read());
60046
+ if (done)
60047
+ break;
60048
+ total += value.byteLength;
60049
+ if (maxBytes !== void 0 && total > maxBytes)
60050
+ throw new ResponseTooLargeError(service, maxBytes);
60051
+ chunks.push(value);
60052
+ }
60053
+ } catch (err) {
60054
+ reader.cancel().catch(() => {
60055
+ });
60056
+ throw err;
60057
+ }
60058
+ const out = new Uint8Array(total);
60059
+ let offset = 0;
60060
+ for (const c of chunks) {
60061
+ out.set(c, offset);
60062
+ offset += c.byteLength;
60063
+ }
60064
+ return out;
60065
+ }
60066
+ function cancelStream(body) {
60067
+ try {
60068
+ body?.cancel().catch(() => {
60069
+ });
60070
+ } catch {
60071
+ }
60072
+ }
60073
+ var ResponseTooLargeError;
60074
+ var init_body_limit = __esm({
60075
+ "node_modules/@chrischall/mcp-utils/dist/http/body-limit.js"() {
60076
+ init_errors();
60077
+ ResponseTooLargeError = class extends McpToolError {
60078
+ maxBytes;
60079
+ constructor(service, maxBytes) {
60080
+ super(`Response from ${service} is larger than the ${maxBytes}-byte limit.`, {
60081
+ hint: "Raise maxBytes (or the client's maxResponseBytes) if a response this large is expected.",
60082
+ kind: "too_large"
60083
+ });
60084
+ this.name = "ResponseTooLargeError";
60085
+ this.maxBytes = maxBytes;
60086
+ }
60087
+ };
60088
+ }
60089
+ });
60090
+
60091
+ // node_modules/@chrischall/mcp-utils/dist/http/fetch-bounded.js
60092
+ var init_fetch_bounded = __esm({
60093
+ "node_modules/@chrischall/mcp-utils/dist/http/fetch-bounded.js"() {
60094
+ init_cancel();
60095
+ init_errors();
60096
+ init_body_limit();
60097
+ init_http();
60098
+ }
60099
+ });
60100
+
60101
+ // node_modules/@chrischall/mcp-utils/dist/http/url-safety.js
60102
+ function isDotSegment(segment) {
60103
+ const s = segment.replace(/%2e/gi, ".");
60104
+ return s === "." || s === "..";
60105
+ }
60106
+ function pathPartOf(path) {
60107
+ const end = path.search(/[?#]/);
60108
+ return end === -1 ? path : path.slice(0, end);
60109
+ }
60110
+ function findPathHazard(path) {
60111
+ const pathPart = pathPartOf(path);
60112
+ if (/[\x00-\x1f\x7f]/.test(pathPart))
60113
+ return "control character";
60114
+ if (pathPart.endsWith(" "))
60115
+ return "trailing space";
60116
+ if (pathPart.includes("\\"))
60117
+ return "backslash";
60118
+ if (pathPart.split("/").some(isDotSegment))
60119
+ return "dot segment";
60120
+ return void 0;
60121
+ }
60122
+ var RedirectRefusedError;
60123
+ var init_url_safety = __esm({
60124
+ "node_modules/@chrischall/mcp-utils/dist/http/url-safety.js"() {
60125
+ init_errors();
60126
+ init_config();
60127
+ RedirectRefusedError = class extends McpToolError {
60128
+ constructor(message) {
60129
+ super(message);
60130
+ this.name = "RedirectRefusedError";
60131
+ }
60132
+ };
60133
+ }
60134
+ });
60135
+
59888
60136
  // node_modules/@chrischall/mcp-utils/dist/internal/retry-after.js
59889
60137
  function retryAfterToMs(header, now = Date.now()) {
59890
60138
  if (header == null)
@@ -59905,6 +60153,14 @@ var init_retry_after = __esm({
59905
60153
  });
59906
60154
 
59907
60155
  // node_modules/@chrischall/mcp-utils/dist/http/index.js
60156
+ function cancelBody(res) {
60157
+ try {
60158
+ const body = res.body;
60159
+ body?.cancel?.()?.catch(() => {
60160
+ });
60161
+ } catch {
60162
+ }
60163
+ }
59908
60164
  function headerOf(headers, name) {
59909
60165
  if (headers === void 0)
59910
60166
  return void 0;
@@ -59930,6 +60186,49 @@ function detectEdgeBlock(input2) {
59930
60186
  }
59931
60187
  return null;
59932
60188
  }
60189
+ function jsonKind(value) {
60190
+ if (value === null)
60191
+ return "null";
60192
+ if (Array.isArray(value))
60193
+ return "array";
60194
+ return typeof value;
60195
+ }
60196
+ function parseJsonBody(text, opts = {}) {
60197
+ const contentType = opts.contentType ?? headerOf(opts.headers, "content-type");
60198
+ const details = {
60199
+ service: opts.service,
60200
+ method: opts.method,
60201
+ path: opts.path,
60202
+ status: opts.status,
60203
+ contentType,
60204
+ expected: opts.expect
60205
+ };
60206
+ if (text.trim().length === 0) {
60207
+ if (opts.expect === void 0)
60208
+ return void 0;
60209
+ throw new UpstreamFormatError({ ...details, received: "empty" });
60210
+ }
60211
+ let value;
60212
+ try {
60213
+ value = JSON.parse(text);
60214
+ } catch (cause) {
60215
+ const edge = detectEdgeBlock({ body: text, headers: opts.headers, status: opts.status });
60216
+ if (edge) {
60217
+ throw new EdgeBlockedError(opts.status ?? 200, edge.vendor, {
60218
+ service: opts.service ?? "the upstream service",
60219
+ method: opts.method,
60220
+ path: opts.path
60221
+ });
60222
+ }
60223
+ throw new UpstreamFormatError({ ...details, expected: void 0, received: "non-json", cause });
60224
+ }
60225
+ if (opts.expect !== void 0) {
60226
+ const kind = jsonKind(value);
60227
+ if (kind !== opts.expect)
60228
+ throw new UpstreamFormatError({ ...details, received: kind });
60229
+ }
60230
+ return value;
60231
+ }
59933
60232
  function httpError(res, text, method, path, service) {
59934
60233
  const edge = detectEdgeBlock({ body: text, headers: res.headers, status: res.status });
59935
60234
  if (edge)
@@ -59980,13 +60279,26 @@ function createApiClient(opts) {
59980
60279
  const sleep2 = opts.sleep ?? defaultSleep;
59981
60280
  const unauthorized = () => opts.onUnauthorized ? opts.onUnauthorized() : new UnauthorizedError(service);
59982
60281
  const retryStatuses = retry.statuses ?? [429];
59983
- const timeoutMs = opts.timeout;
60282
+ const timeoutMs = opts.timeout === false ? 0 : opts.timeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
60283
+ const redirectMode = opts.redirect;
60284
+ const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
60285
+ const reportWriteOutcome = opts.writeOutcomeUnknown !== false;
60286
+ const defaultMaxBytes = checkMaxBytes("maxResponseBytes", opts.maxResponseBytes);
60287
+ if (!Number.isSafeInteger(maxRedirects) || maxRedirects < 0) {
60288
+ throw new TypeError(`createApiClient: maxRedirects must be a non-negative integer, got ${String(maxRedirects)}.`);
60289
+ }
59984
60290
  const authHeader = (token) => !token ? {} : opts.tokenHeader ? { [opts.tokenHeader]: token } : { Authorization: `Bearer ${token}` };
60291
+ const transportFailures = /* @__PURE__ */ new WeakSet();
60292
+ const markTransportFailure = (err) => {
60293
+ if (typeof err === "object" && err !== null && err.name !== "AbortError") {
60294
+ transportFailures.add(err);
60295
+ }
60296
+ };
59985
60297
  async function withTimeout(run) {
59986
- if (timeoutMs == null || timeoutMs <= 0) {
60298
+ if (timeoutMs <= 0) {
59987
60299
  const res2 = await run(withAmbientCancellation(void 0));
59988
60300
  return { res: res2, done: () => {
59989
- }, expired: void 0 };
60301
+ }, expired: void 0, timedOut: () => false };
59990
60302
  }
59991
60303
  const controller = new AbortController();
59992
60304
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -60007,16 +60319,11 @@ function createApiClient(opts) {
60007
60319
  });
60008
60320
  expired.catch(() => {
60009
60321
  });
60010
- return { res, done: () => clearTimeout(timer), expired };
60322
+ return { res, done: () => clearTimeout(timer), expired, timedOut: () => controller.signal.aborted };
60011
60323
  }
60012
60324
  function discard(attempt) {
60013
60325
  attempt.done();
60014
- try {
60015
- const body = attempt.res.body;
60016
- body?.cancel?.()?.catch(() => {
60017
- });
60018
- } catch {
60019
- }
60326
+ cancelBody(attempt.res);
60020
60327
  }
60021
60328
  async function readBody(attempt, read) {
60022
60329
  try {
@@ -60024,15 +60331,40 @@ function createApiClient(opts) {
60024
60331
  return await read(attempt.res);
60025
60332
  return await Promise.race([read(attempt.res), attempt.expired]);
60026
60333
  } catch (err) {
60027
- if (err instanceof RequestTimeoutError) {
60028
- attempt.res.body?.cancel().catch(() => {
60029
- });
60334
+ const timeout = err instanceof RequestTimeoutError || err instanceof Error && err.name === "AbortError" && attempt.timedOut();
60335
+ if (timeout) {
60336
+ cancelBody(attempt.res);
60337
+ throw err instanceof RequestTimeoutError ? err : new RequestTimeoutError(service, timeoutMs);
60030
60338
  }
60339
+ if (!(err instanceof ResponseTooLargeError))
60340
+ markTransportFailure(err);
60031
60341
  throw err;
60032
60342
  } finally {
60033
60343
  attempt.done();
60034
60344
  }
60035
60345
  }
60346
+ function readBytes(attempt, maxBytes) {
60347
+ if (maxBytes === void 0)
60348
+ return readBody(attempt, async (r) => new Uint8Array(await r.arrayBuffer()));
60349
+ const race = (p) => attempt.expired ? Promise.race([p, attempt.expired]) : p;
60350
+ return readBody(attempt, (r) => readBytesCapped(r, maxBytes, service, race));
60351
+ }
60352
+ async function readText(attempt, maxBytes) {
60353
+ if (maxBytes === void 0)
60354
+ return readBody(attempt, (r) => r.text());
60355
+ return new TextDecoder().decode(await readBytes(attempt, maxBytes));
60356
+ }
60357
+ function readErrorText(attempt, maxBytes) {
60358
+ return readText(attempt, maxBytes).catch((err) => {
60359
+ if (err instanceof ResponseTooLargeError)
60360
+ return "";
60361
+ throw err;
60362
+ });
60363
+ }
60364
+ function scanText(attempt, maxBytes) {
60365
+ return readText(attempt, maxBytes).catch(() => "");
60366
+ }
60367
+ const capOf = (opt) => opt.maxBytes !== void 0 ? checkMaxBytes("maxBytes", opt.maxBytes) : defaultMaxBytes;
60036
60368
  const baseParts = (() => {
60037
60369
  try {
60038
60370
  const u = new URL(base);
@@ -60043,6 +60375,14 @@ function createApiClient(opts) {
60043
60375
  })();
60044
60376
  const baseOrigin = baseParts?.origin;
60045
60377
  function resolveUrl2(path, query) {
60378
+ const hazard = findPathHazard(path);
60379
+ if (hazard !== void 0) {
60380
+ throw new Error(
60381
+ // Quote only the path part: this fires on hostile input, and the
60382
+ // query may carry values that do not belong in a log line.
60383
+ `Refusing request to ${service}: path ${JSON.stringify(path.split(/[?#]/, 1)[0])} contains a ${hazard}, which URL normalisation would rewrite. Build paths with apiPath\`...\` so each value is one encoded segment.`
60384
+ );
60385
+ }
60046
60386
  const url4 = `${base}${path}${query}`;
60047
60387
  if (baseOrigin === void 0)
60048
60388
  return url4;
@@ -60057,23 +60397,66 @@ function createApiClient(opts) {
60057
60397
  }
60058
60398
  return url4;
60059
60399
  }
60060
- async function send(method, path, opt) {
60400
+ function sameOriginHop(location, from) {
60401
+ let next;
60402
+ try {
60403
+ next = new URL(location, from);
60404
+ } catch {
60405
+ throw new RedirectRefusedError(`Refusing redirect from ${service}: the Location header is not a valid URL.`);
60406
+ }
60407
+ if (baseOrigin === void 0 || next.origin !== baseOrigin || next.username !== baseParts?.username || next.password !== baseParts?.password) {
60408
+ throw new RedirectRefusedError(`Refusing redirect from ${service} to ${next.origin}: it leaves the base origin ${baseOrigin ?? "(unparseable)"}, and following it would re-send the credential there.`);
60409
+ }
60410
+ return next.href;
60411
+ }
60412
+ async function send(method, path, opt, dispatch) {
60061
60413
  const { body: reqBody, contentType } = encodeBody(opt);
60062
60414
  const query = opt.query ? buildQueryString(opt.query) : "";
60063
60415
  const url4 = resolveUrl2(path, query);
60064
60416
  const bodyInit = reqBody !== void 0 ? { body: reqBody } : {};
60065
- const fetchWith = (token, signal) => doFetch(url4, {
60066
- method,
60067
- headers: {
60068
- Accept: "application/json",
60069
- ...contentType !== void 0 ? { "Content-Type": contentType } : {},
60070
- ...opts.baseHeaders,
60071
- ...authHeader(token || void 0),
60072
- ...opt.headers
60073
- },
60074
- ...signal ? { signal } : {},
60075
- ...bodyInit
60076
- });
60417
+ const fetchHop = (hopUrl, hopMethod, withBody, token, signal) => {
60418
+ if (!signal?.aborted)
60419
+ dispatch.sent = true;
60420
+ return doFetch(hopUrl, {
60421
+ method: hopMethod,
60422
+ headers: {
60423
+ Accept: "application/json",
60424
+ ...withBody && contentType !== void 0 ? { "Content-Type": contentType } : {},
60425
+ ...opts.baseHeaders,
60426
+ ...authHeader(token || void 0),
60427
+ ...opt.headers
60428
+ },
60429
+ ...signal ? { signal } : {},
60430
+ ...redirectMode !== void 0 ? { redirect: redirectMode === "same-origin" ? "manual" : redirectMode } : {},
60431
+ ...withBody ? bodyInit : {}
60432
+ }).catch((err) => {
60433
+ markTransportFailure(err);
60434
+ throw err;
60435
+ });
60436
+ };
60437
+ const fetchWith = async (token, signal) => {
60438
+ if (redirectMode !== "same-origin")
60439
+ return fetchHop(url4, method, true, token, signal);
60440
+ let hopUrl = url4;
60441
+ let hopMethod = method;
60442
+ let withBody = true;
60443
+ for (let hops = 0; ; hops += 1) {
60444
+ const res = await fetchHop(hopUrl, hopMethod, withBody, token, signal);
60445
+ const location = REDIRECT_STATUSES.has(res.status) ? responseHeader(res, "location") : void 0;
60446
+ if (location === void 0 || location === null)
60447
+ return res;
60448
+ cancelBody(res);
60449
+ if (hops >= maxRedirects) {
60450
+ throw new RedirectRefusedError(`Refusing request to ${service}: too many redirects (more than ${maxRedirects}).`);
60451
+ }
60452
+ hopUrl = sameOriginHop(location, hopUrl);
60453
+ if (res.status === 303 || (res.status === 301 || res.status === 302) && hopMethod === "POST") {
60454
+ if (hopMethod !== "HEAD")
60455
+ hopMethod = "GET";
60456
+ withBody = false;
60457
+ }
60458
+ }
60459
+ };
60077
60460
  const once = () => withTimeout((signal) => opts.tokenManager ? opts.tokenManager.withAuth((token) => fetchWith(token, signal)) : (async () => fetchWith(await opts.getToken?.() || void 0, signal))());
60078
60461
  let attempt = 0;
60079
60462
  for (; ; ) {
@@ -60092,20 +60475,20 @@ function createApiClient(opts) {
60092
60475
  return current;
60093
60476
  }
60094
60477
  }
60095
- async function unauthorizedOrEdge(attempt, method, path) {
60478
+ async function unauthorizedOrEdge(attempt, method, path, maxBytes) {
60096
60479
  const res = attempt.res;
60097
60480
  let text = "";
60098
60481
  if (/json/i.test(responseHeader(res, "content-type") ?? "")) {
60099
60482
  discard(attempt);
60100
60483
  } else {
60101
- text = await readBody(attempt, (r) => r.text()).catch(() => "");
60484
+ text = await scanText(attempt, maxBytes);
60102
60485
  }
60103
60486
  const edge = detectEdgeBlock({ body: text, headers: res.headers, status: res.status });
60104
60487
  if (edge)
60105
60488
  return new EdgeBlockedError(res.status, edge.vendor, { service, method, path });
60106
60489
  return unauthorized();
60107
60490
  }
60108
- async function rateLimitedError(attempt, method, path) {
60491
+ async function rateLimitedError(attempt, method, path, maxBytes) {
60109
60492
  const res = attempt.res;
60110
60493
  const retryAfter = responseHeader(res, "retry-after");
60111
60494
  const retryAfterMs = retryAfterToMs(retryAfter);
@@ -60117,61 +60500,92 @@ function createApiClient(opts) {
60117
60500
  if (/json/i.test(responseHeader(res, "content-type") ?? "")) {
60118
60501
  discard(attempt);
60119
60502
  } else {
60120
- text = await readBody(attempt, (r) => r.text()).catch(() => "");
60503
+ text = await scanText(attempt, maxBytes);
60121
60504
  }
60122
60505
  const edgeBlock = detectEdgeBlock({ body: text, headers: res.headers, status: res.status });
60123
60506
  return opts.onRateLimited({ status: res.status, retryAfter, retryAfterMs, edgeBlock, method, path });
60124
60507
  }
60125
- async function fetchJson(method, path, opt = {}) {
60126
- const attempt = await send(method, path, opt);
60508
+ async function guardWrite(method, opt, call) {
60509
+ const dispatch = { sent: false };
60510
+ try {
60511
+ return await call(dispatch);
60512
+ } catch (err) {
60513
+ const write = reportWriteOutcome && opt.idempotent !== true && !SAFE_METHODS.has(method.toUpperCase());
60514
+ if (!write || !dispatch.sent)
60515
+ throw err;
60516
+ if (err instanceof RequestTimeoutError) {
60517
+ throw new WriteOutcomeUnknownError(service, method, {
60518
+ timeoutMs: err.timeoutMs,
60519
+ cause: err,
60520
+ hint: opts.writeOutcomeHint
60521
+ });
60522
+ }
60523
+ if (typeof err === "object" && err !== null && transportFailures.has(err)) {
60524
+ throw new WriteOutcomeUnknownError(service, method, { cause: err, hint: opts.writeOutcomeHint });
60525
+ }
60526
+ throw err;
60527
+ }
60528
+ }
60529
+ const fetchJson = (method, path, opt = {}) => guardWrite(method, opt, (dispatch) => fetchJsonOnce(method, path, opt, dispatch));
60530
+ const fetchHtml = (method, path, opt = {}) => guardWrite(method, opt, (dispatch) => fetchHtmlOnce(method, path, opt, dispatch));
60531
+ const fetchRaw = (method, path, opt = {}) => guardWrite(method, opt, (dispatch) => fetchRawOnce(method, path, opt, dispatch));
60532
+ async function fetchJsonOnce(method, path, opt, dispatch) {
60533
+ const maxBytes = capOf(opt);
60534
+ const attempt = await send(method, path, opt, dispatch);
60127
60535
  const res = attempt.res;
60128
60536
  if (res.status === 401)
60129
- throw await unauthorizedOrEdge(attempt, method, path);
60537
+ throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
60130
60538
  if (res.status === 429)
60131
- throw await rateLimitedError(attempt, method, path);
60539
+ throw await rateLimitedError(attempt, method, path, maxBytes);
60132
60540
  if (res.status === 204) {
60133
60541
  discard(attempt);
60134
- return void 0;
60542
+ return parseJsonBody("", { expect: opt.expect, service, method, path, status: 204 });
60135
60543
  }
60136
- const text = await readBody(attempt, (r) => r.text());
60137
60544
  if (!res.ok) {
60138
- throw httpError(res, text, method, path, service);
60545
+ throw httpError(res, await readErrorText(attempt, maxBytes), method, path, service);
60139
60546
  }
60140
- if (text.length === 0)
60141
- return void 0;
60142
- return JSON.parse(text);
60547
+ const text = await readText(attempt, maxBytes);
60548
+ return parseJsonBody(text, {
60549
+ expect: opt.expect,
60550
+ service,
60551
+ method,
60552
+ path,
60553
+ status: res.status,
60554
+ headers: res.headers
60555
+ });
60143
60556
  }
60144
- async function fetchHtml(method, path, opt = {}) {
60557
+ async function fetchHtmlOnce(method, path, opt, dispatch) {
60558
+ const maxBytes = capOf(opt);
60145
60559
  const headers = { Accept: "text/html,*/*", ...opt.headers };
60146
- const attempt = await send(method, path, { ...opt, headers });
60560
+ const attempt = await send(method, path, { ...opt, headers }, dispatch);
60147
60561
  const res = attempt.res;
60148
60562
  if (res.status === 401)
60149
- throw await unauthorizedOrEdge(attempt, method, path);
60563
+ throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
60150
60564
  if (res.status === 429)
60151
- throw await rateLimitedError(attempt, method, path);
60152
- const text = await readBody(attempt, (r) => r.text());
60565
+ throw await rateLimitedError(attempt, method, path, maxBytes);
60153
60566
  if (!res.ok) {
60154
- throw httpError(res, text, method, path, service);
60567
+ throw httpError(res, await readErrorText(attempt, maxBytes), method, path, service);
60155
60568
  }
60156
- return text;
60569
+ return readText(attempt, maxBytes);
60157
60570
  }
60158
- async function fetchRaw(method, path, opt = {}) {
60571
+ async function fetchRawOnce(method, path, opt, dispatch) {
60572
+ const maxBytes = capOf(opt);
60159
60573
  const headers = { Accept: "*/*", ...opt.headers };
60160
- const attempt = await send(method, path, { ...opt, headers });
60574
+ const attempt = await send(method, path, { ...opt, headers }, dispatch);
60161
60575
  const res = attempt.res;
60162
60576
  if (res.status === 401)
60163
- throw await unauthorizedOrEdge(attempt, method, path);
60577
+ throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
60164
60578
  if (res.status === 429)
60165
- throw await rateLimitedError(attempt, method, path);
60579
+ throw await rateLimitedError(attempt, method, path, maxBytes);
60166
60580
  if (!res.ok) {
60167
- const text = await readBody(attempt, (r) => r.text()).catch((err) => {
60581
+ const text = await readText(attempt, maxBytes).catch((err) => {
60168
60582
  if (err instanceof RequestTimeoutError)
60169
60583
  throw err;
60170
60584
  return "";
60171
60585
  });
60172
60586
  throw httpError(res, text, method, path, service);
60173
60587
  }
60174
- const bytes = new Uint8Array(await readBody(attempt, (r) => r.arrayBuffer()));
60588
+ const bytes = await readBytes(attempt, maxBytes);
60175
60589
  return {
60176
60590
  status: res.status,
60177
60591
  contentType: responseHeader(res, "content-type"),
@@ -60181,6 +60595,14 @@ function createApiClient(opts) {
60181
60595
  }
60182
60596
  return { fetchJson, fetchHtml, fetchRaw };
60183
60597
  }
60598
+ function checkMaxBytes(name, value) {
60599
+ if (value === void 0)
60600
+ return void 0;
60601
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
60602
+ throw new TypeError(`createApiClient: ${name} must be a non-negative number, got ${String(value)}.`);
60603
+ }
60604
+ return value === Infinity ? void 0 : value;
60605
+ }
60184
60606
  function buildQueryString(params) {
60185
60607
  const parts = [];
60186
60608
  for (const [key, value] of Object.entries(params)) {
@@ -60204,7 +60626,7 @@ function formatApiError(status, method, path, errorText, opts = {}) {
60204
60626
  const safe = truncateErrorMessage(errorText ?? "", opts.max).trim();
60205
60627
  return safe.length > 0 ? `${head}: ${safe}` : head;
60206
60628
  }
60207
- var DEFAULT_RETRY, defaultSleep, UnauthorizedError, RateLimitedError, RequestTimeoutError, ApiError, EdgeBlockedError, EDGE_BLOCK_SCAN_MAX, EDGE_BLOCK_MARKERS;
60629
+ var DEFAULT_RETRY, defaultSleep, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_MAX_REDIRECTS, REDIRECT_STATUSES, UnauthorizedError, RateLimitedError, RequestTimeoutError, DEFAULT_WRITE_OUTCOME_HINT, WriteOutcomeUnknownError, SAFE_METHODS, ApiError, EdgeBlockedError, EDGE_BLOCK_SCAN_MAX, EDGE_BLOCK_MARKERS;
60208
60630
  var init_http = __esm({
60209
60631
  "node_modules/@chrischall/mcp-utils/dist/http/index.js"() {
60210
60632
  init_headers();
@@ -60214,12 +60636,21 @@ var init_http = __esm({
60214
60636
  init_throttle();
60215
60637
  init_response_cache();
60216
60638
  init_net_atoms();
60639
+ init_fetch_bounded();
60640
+ init_url_safety();
60641
+ init_body_limit();
60217
60642
  init_net_atoms();
60643
+ init_url_safety();
60218
60644
  init_retry_after();
60219
60645
  DEFAULT_RETRY = { count: 1, delayMs: 2e3 };
60220
60646
  defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
60647
+ DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
60648
+ DEFAULT_MAX_REDIRECTS = 5;
60649
+ REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
60221
60650
  UnauthorizedError = class extends Error {
60222
60651
  status = 401;
60652
+ /** Always `'credential_rejected'` unless a subclass says otherwise. */
60653
+ kind = "credential_rejected";
60223
60654
  constructor(service) {
60224
60655
  super(`Unauthorized (401) from ${service} \u2014 the token is missing, invalid, or expired.`);
60225
60656
  this.name = "UnauthorizedError";
@@ -60228,6 +60659,8 @@ var init_http = __esm({
60228
60659
  };
60229
60660
  RateLimitedError = class extends Error {
60230
60661
  status = 429;
60662
+ /** `'http'`: the far side answered, with a 429 (see `status`). */
60663
+ kind = "http";
60231
60664
  retryAfterMs;
60232
60665
  constructor(service, retryAfterMs) {
60233
60666
  super(`Rate limited (429) by ${service} after retries.`);
@@ -60238,6 +60671,8 @@ var init_http = __esm({
60238
60671
  };
60239
60672
  RequestTimeoutError = class extends Error {
60240
60673
  timeoutMs;
60674
+ /** Always `'timeout'` unless a subclass says otherwise. */
60675
+ kind = "timeout";
60241
60676
  constructor(service, timeoutMs) {
60242
60677
  super(`Request to ${service} timed out after ${timeoutMs}ms.`);
60243
60678
  this.name = "RequestTimeoutError";
@@ -60245,6 +60680,33 @@ var init_http = __esm({
60245
60680
  Object.setPrototypeOf(this, new.target.prototype);
60246
60681
  }
60247
60682
  };
60683
+ DEFAULT_WRITE_OUTCOME_HINT = "The write may have happened \u2014 check before retrying; do not resend blindly.";
60684
+ WriteOutcomeUnknownError = class extends McpToolError {
60685
+ /** Always `true`: whether the write happened is not known. */
60686
+ outcomeUnknown = true;
60687
+ /** Always `false`: never replay this request without checking first. */
60688
+ retrySafe = false;
60689
+ /** `true` when the request's timeout expired; `false` for a dropped connection or broken body. */
60690
+ timedOut;
60691
+ /** The timeout that expired, when {@link timedOut}. */
60692
+ timeoutMs;
60693
+ /** The request method, upper-cased. */
60694
+ method;
60695
+ constructor(service, method, opts = {}) {
60696
+ const verb = method.toUpperCase();
60697
+ const what = opts.timeoutMs !== void 0 ? `${verb} to ${service} timed out after ${opts.timeoutMs}ms` : `${verb} to ${service} failed: ${truncateErrorMessage(messageOf(opts.cause), 200)}`;
60698
+ super(`${what} \u2014 the write may already have been applied (outcome is unknown).`, {
60699
+ hint: opts.hint ?? DEFAULT_WRITE_OUTCOME_HINT,
60700
+ cause: opts.cause,
60701
+ kind: opts.timeoutMs !== void 0 ? "timeout" : "transport"
60702
+ });
60703
+ this.name = "WriteOutcomeUnknownError";
60704
+ this.timedOut = opts.timeoutMs !== void 0;
60705
+ this.timeoutMs = opts.timeoutMs;
60706
+ this.method = verb;
60707
+ }
60708
+ };
60709
+ SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
60248
60710
  ApiError = class extends Error {
60249
60711
  status;
60250
60712
  constructor(status, message) {
@@ -60257,6 +60719,8 @@ var init_http = __esm({
60257
60719
  EdgeBlockedError = class extends ApiError {
60258
60720
  /** Which edge refused the request, e.g. `'CloudFront'` or `'Cloudflare'`. */
60259
60721
  vendor;
60722
+ /** Always `'edge_blocked'`: the credential was never judged. */
60723
+ kind = "edge_blocked";
60260
60724
  /**
60261
60725
  * @param where The service the request was for, and — when the thrower knows
60262
60726
  * it — the method and path, which the message names.
@@ -60320,6 +60784,7 @@ var init_concurrency = __esm({
60320
60784
  // node_modules/@chrischall/mcp-utils/dist/dates/index.js
60321
60785
  var init_dates = __esm({
60322
60786
  "node_modules/@chrischall/mcp-utils/dist/dates/index.js"() {
60787
+ init_config();
60323
60788
  }
60324
60789
  });
60325
60790
 
@@ -60352,7 +60817,7 @@ var init_zod2 = __esm({
60352
60817
  NonNegInt = external_exports.number().int().nonnegative();
60353
60818
  NonEmptyString = external_exports.string().min(1);
60354
60819
  IsoDate = external_exports.iso.date();
60355
- IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
60820
+ IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30").overwrite((v) => v.padStart(5, "0"));
60356
60821
  NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
60357
60822
  SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
60358
60823
  schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
@@ -60387,6 +60852,7 @@ var init_auth = __esm({
60387
60852
  init_config();
60388
60853
  init_errors();
60389
60854
  init_http();
60855
+ init_cancel();
60390
60856
  init_cached_token();
60391
60857
  init_es256();
60392
60858
  }
@@ -60438,7 +60904,8 @@ __export(images_exports, {
60438
60904
  });
60439
60905
  import { readFile, stat } from "node:fs/promises";
60440
60906
  import { existsSync as existsSync3 } from "node:fs";
60441
- import { delimiter, join as join6, resolve as resolve4, isAbsolute as isAbsolute4 } from "node:path";
60907
+ import { homedir as homedir3 } from "node:os";
60908
+ import { delimiter, join as join7, resolve as resolve4, isAbsolute as isAbsolute4 } from "node:path";
60442
60909
  function uploadRoots() {
60443
60910
  const roots = readEnvVar("GEMINI_UPLOAD_DIR")?.split(delimiter).map((r) => r.trim()).filter(Boolean);
60444
60911
  return roots && roots.length > 0 ? roots : void 0;
@@ -60459,7 +60926,7 @@ function referenceRoots(env = process.env) {
60459
60926
  let roots = rootList("GEMINI_REFERENCE_DIR", env) ?? rootList("GEMINI_UPLOAD_DIR", env);
60460
60927
  if (!roots) {
60461
60928
  const dataDir = readEnvVar("MCP_DATA_DIR", { env });
60462
- if (dataDir) roots = [join6(dataDir, "uploads")];
60929
+ if (dataDir) roots = [join7(dataDir, "uploads")];
60463
60930
  }
60464
60931
  if (!roots) return void 0;
60465
60932
  const output2 = readEnvVar("GEMINI_OUTPUT_DIR", { env })?.trim();
@@ -60535,7 +61002,7 @@ function resolveInputPath(p, kind) {
60535
61002
  if (isAbsolute4(p) && existsSync3(p)) return p;
60536
61003
  const inputDir = readEnvVar("GEMINI_INPUT_DIR");
60537
61004
  if (inputDir) {
60538
- const candidate2 = join6(inputDir, p);
61005
+ const candidate2 = join7(inputDir, p);
60539
61006
  if (existsSync3(candidate2)) return resolve4(candidate2);
60540
61007
  }
60541
61008
  const cwd = resolve4(p);
@@ -60629,11 +61096,14 @@ function baseName(name) {
60629
61096
  const safe = stripped.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
60630
61097
  return safe || "image";
60631
61098
  }
61099
+ function defaultOutputDir2() {
61100
+ return join7(homedir3(), "Downloads", OUTPUT_DIR_NAME);
61101
+ }
60632
61102
  function lookupOutputDir(perCall) {
60633
61103
  const trimmed = perCall?.trim() || void 0;
60634
61104
  const configured = readEnvVar("GEMINI_OUTPUT_DIR");
60635
61105
  const raw = trimmed ?? configured;
60636
- if (!raw) return process.cwd();
61106
+ if (!raw) return defaultOutputDir2();
60637
61107
  const dir = expandPath(raw);
60638
61108
  if (trimmed && configured) assertPathWithinRoots(dir, [configured]);
60639
61109
  return dir;
@@ -60641,10 +61111,11 @@ function lookupOutputDir(perCall) {
60641
61111
  function resolveOutputDir2(perCall) {
60642
61112
  const configured = readEnvVar("GEMINI_OUTPUT_DIR");
60643
61113
  return resolveOutputDir(perCall?.trim() || void 0, "GEMINI_OUTPUT_DIR", {
61114
+ name: OUTPUT_DIR_NAME,
60644
61115
  ...configured ? { allowedRoots: [configured] } : {}
60645
61116
  });
60646
61117
  }
60647
- var REFERENCE_IMAGE_MIMES, VIDEO_MIME_BY_EXT, UPLOAD_MIME_BY_EXT;
61118
+ var REFERENCE_IMAGE_MIMES, VIDEO_MIME_BY_EXT, UPLOAD_MIME_BY_EXT, OUTPUT_DIR_NAME;
60648
61119
  var init_images = __esm({
60649
61120
  "src/images.ts"() {
60650
61121
  "use strict";
@@ -60679,6 +61150,7 @@ var init_images = __esm({
60679
61150
  flac: "audio/flac",
60680
61151
  ...VIDEO_MIME_BY_EXT
60681
61152
  };
61153
+ OUTPUT_DIR_NAME = "gemini-mcp";
60682
61154
  }
60683
61155
  });
60684
61156
 
@@ -60691,7 +61163,7 @@ import { execFile } from "node:child_process";
60691
61163
  import { promisify } from "node:util";
60692
61164
  import { readFile as readFile2, unlink } from "node:fs/promises";
60693
61165
  import { tmpdir } from "node:os";
60694
- import { join as join7 } from "node:path";
61166
+ import { join as join8 } from "node:path";
60695
61167
  async function readClipboardImage(run = defaultRun) {
60696
61168
  if (process.platform !== "darwin") {
60697
61169
  throw new McpToolError("from_clipboard is only supported on macOS", {
@@ -60699,8 +61171,8 @@ async function readClipboardImage(run = defaultRun) {
60699
61171
  });
60700
61172
  }
60701
61173
  const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
60702
- const png = join7(tmpdir(), `gemini-clip-${id}.png`);
60703
- const jpg = join7(tmpdir(), `gemini-clip-${id}.jpg`);
61174
+ const png = join8(tmpdir(), `gemini-clip-${id}.png`);
61175
+ const jpg = join8(tmpdir(), `gemini-clip-${id}.jpg`);
60704
61176
  try {
60705
61177
  try {
60706
61178
  await run("osascript", [
@@ -60747,15 +61219,15 @@ init_dist2();
60747
61219
 
60748
61220
  // src/dotenv.ts
60749
61221
  init_dist2();
60750
- import { dirname as dirname3, join as join5 } from "node:path";
61222
+ import { dirname as dirname3, join as join6 } from "node:path";
60751
61223
  import { fileURLToPath } from "node:url";
60752
61224
  async function loadStdioDotenv() {
60753
61225
  const here = dirname3(fileURLToPath(import.meta.url));
60754
- await loadDotenvSafely({ path: join5(here, "..", ".env"), override: false });
61226
+ await loadDotenvSafely({ path: join6(here, "..", ".env"), override: false });
60755
61227
  }
60756
61228
 
60757
61229
  // src/version.ts
60758
- var VERSION = "2.3.2";
61230
+ var VERSION = "2.3.4";
60759
61231
 
60760
61232
  // src/errors.ts
60761
61233
  init_dist2();
@@ -60822,7 +61294,7 @@ function registerModelTools(server, client2) {
60822
61294
  "gemini_list_models",
60823
61295
  {
60824
61296
  description: "List the Gemini image-generation models available to your API key (Nano Banana / Nano Banana Pro family), and the current default model.",
60825
- annotations: { readOnlyHint: true }
61297
+ annotations: toolAnnotations({ readOnly: true, openWorld: true })
60826
61298
  },
60827
61299
  async () => {
60828
61300
  const models = await client2.listModels();
@@ -60866,12 +61338,6 @@ function statusOf(err) {
60866
61338
 
60867
61339
  // node_modules/@chrischall/mcp-utils/dist/healthcheck/index.js
60868
61340
  init_zod();
60869
- function statusOf2(err) {
60870
- if (typeof err !== "object" || err === null)
60871
- return void 0;
60872
- const s = err.status ?? err.statusCode;
60873
- return typeof s === "number" ? s : void 0;
60874
- }
60875
61341
  var CREDENTIAL_ARMS = /* @__PURE__ */ new Set([
60876
61342
  "ok",
60877
61343
  "no_credential",
@@ -60887,6 +61353,9 @@ var CREDENTIAL_ARMS = /* @__PURE__ */ new Set([
60887
61353
  function isArm(kind) {
60888
61354
  return kind !== void 0 && CREDENTIAL_ARMS.has(kind);
60889
61355
  }
61356
+ function fromSource(source) {
61357
+ return source !== null ? ` from '${source}'` : "";
61358
+ }
60890
61359
  function credentialHint(arm, prefix, hostLabel, source) {
60891
61360
  switch (arm) {
60892
61361
  case "ok":
@@ -60894,15 +61363,15 @@ function credentialHint(arm, prefix, hostLabel, source) {
60894
61363
  case "no_credential":
60895
61364
  return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
60896
61365
  case "credential_rejected":
60897
- return `${hostLabel} rejected the credential from '${source}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
61366
+ return `${hostLabel} rejected the credential${fromSource(source)}. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
60898
61367
  case "edge_blocked":
60899
61368
  return `${hostLabel} refused the request at its CDN/WAF before it reached the API, so the credential${source !== null ? ` from '${source}'` : ""} was never judged \u2014 re-signing in or rotating it will not help. This is usually a block on this host's IP address or request fingerprint: route through the ContextMint Bridge if this connector supports it, or retry later from a different network.`;
60900
61369
  case "session_expired":
60901
- return `The credential from '${source}' is configured, but no session is live \u2014 ${hostLabel} served a sign-in page rather than the data. Sign in again; a cookie-session portal expires these on its own, so this recurs between uses.`;
61370
+ return `The credential${fromSource(source)} is configured, but no session is live \u2014 ${hostLabel} served a sign-in page rather than the data. Sign in again; a cookie-session portal expires these on its own, so this recurs between uses.`;
60902
61371
  case "verification_pending":
60903
61372
  return `${hostLabel} is holding the sign-in on a second factor rather than refusing it. Supply the verification code the ACCOUNT HOLDER received \u2014 the credential itself is not the problem, so changing it will not help.`;
60904
61373
  case "timeout":
60905
- return `The credential from '${source}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
61374
+ return `${source !== null ? `The credential from '${source}' resolved, but ` : ""}${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
60906
61375
  case "http":
60907
61376
  return `${hostLabel} answered with an error status that is not an auth rejection. That is USUALLY a ${hostLabel}-side problem rather than an auth one \u2014 but a 404 here more often means the probe path is wrong than that ${hostLabel} is broken, so check error.message and probe.url before concluding anything about the credential.`;
60908
61377
  case "transport":
@@ -60923,7 +61392,8 @@ async function runCredentialHealthcheck(args) {
60923
61392
  state = await resolveCredential();
60924
61393
  } catch (e) {
60925
61394
  const edge = edgeBlockOf(e);
60926
- const classified = classifyThrown?.(e) ?? (edge !== null ? { kind: "edge_blocked", detail: { vendor: edge.vendor } } : void 0);
61395
+ const declared = errorKindOf(e);
61396
+ const classified = classifyThrown?.(e) ?? (edge !== null ? { kind: "edge_blocked", detail: { vendor: edge.vendor } } : declared !== void 0 ? { kind: declared } : void 0);
60927
61397
  const result2 = {
60928
61398
  ok: false,
60929
61399
  // Still false, and still no source: a classification explains WHY
@@ -60970,10 +61440,11 @@ async function runCredentialHealthcheck(args) {
60970
61440
  try {
60971
61441
  await probeFn();
60972
61442
  } catch (e) {
60973
- status = statusOf2(e);
61443
+ status = errorStatusOf(e);
60974
61444
  const aborted2 = e instanceof Error && e.name === "AbortError";
60975
61445
  const edge = edgeBlockOf(e);
60976
- arm = edge !== null ? "edge_blocked" : status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
61446
+ const declared = errorKindOf(e);
61447
+ arm = edge !== null ? "edge_blocked" : declared !== void 0 ? declared : status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
60977
61448
  let kind = arm;
60978
61449
  let detail = edge !== null ? { vendor: edge.vendor } : void 0;
60979
61450
  const custom2 = classifyThrown?.(e);
@@ -61176,10 +61647,12 @@ function assertPublicHttpsUrl(raw, requested) {
61176
61647
  }
61177
61648
  const rawHost = url4.hostname.toLowerCase();
61178
61649
  const isIpv6Literal = rawHost.startsWith("[") && rawHost.endsWith("]");
61179
- const host = isIpv6Literal ? rawHost.slice(1, -1) : rawHost;
61650
+ const host = isIpv6Literal ? rawHost.slice(1, -1) : rawHost.replace(/\.+$/, "");
61180
61651
  const v4 = isIpv6Literal ? void 0 : ipv4Octets(host);
61181
61652
  const v6 = isIpv6Literal ? ipv6Groups(host) : void 0;
61182
- const blocked = isIpv6Literal ? v6 === void 0 || isPrivateIpv6(v6) : host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || (v4 ? isPrivateIpv4(v4) : false);
61653
+ const blocked = isIpv6Literal ? v6 === void 0 || isPrivateIpv6(v6) : host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || // Private-network names: Fly's 6PN (`<app>.internal`, where the hosted
61654
+ // build runs) and GCP's metadata server (`metadata.google.internal`).
61655
+ host === "internal" || host.endsWith(".internal") || (v4 ? isPrivateIpv4(v4) : false);
61183
61656
  if (blocked) {
61184
61657
  throw new McpToolError(
61185
61658
  `Refusing to fetch ${describeUrl(raw, requested)}: "${url4.hostname}" is a private, loopback or link-local address, not a public host.`,
@@ -61526,10 +61999,12 @@ async function readPin(bucket) {
61526
61999
  }
61527
62000
  function pinnedTenant(bucket, derive) {
61528
62001
  let cached2;
61529
- async function resolve5() {
62002
+ async function resolve6() {
61530
62003
  const pinned = await readPin(bucket);
61531
62004
  if (pinned) return { tenant: pinned, pinned: true };
61532
- const tenant = await derive();
62005
+ const derived2 = await derive();
62006
+ if (typeof derived2 !== "string") return { tenant: derived2.tenant, pinned: false };
62007
+ const tenant = derived2;
61533
62008
  try {
61534
62009
  await bucket.put(TENANT_PIN_KEY, JSON.stringify({ tenant }), {
61535
62010
  httpMetadata: { contentType: "application/json" }
@@ -61541,7 +62016,7 @@ function pinnedTenant(bucket, derive) {
61541
62016
  }
61542
62017
  return () => {
61543
62018
  if (cached2) return cached2;
61544
- const attempt = resolve5().then(({ tenant, pinned }) => {
62019
+ const attempt = resolve6().then(({ tenant, pinned }) => {
61545
62020
  if (!pinned && cached2 === attempt) cached2 = void 0;
61546
62021
  return tenant;
61547
62022
  });
@@ -62028,7 +62503,7 @@ var sharedImageSchema = {
62028
62503
  aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Exact output aspect ratio. For a plain landscape/portrait/square request, `orientation` is the shorthand; this wins if both are given."),
62029
62504
  orientation: orientationSchema,
62030
62505
  image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
62031
- output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
62506
+ output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or ~/Downloads/gemini-mcp)"),
62032
62507
  inline: external_exports.boolean().optional().describe("Return the image as an inline image block you can SEE, instead of a path (stdio) or link (hosted). The default costs nothing to carry and hands back a reference you can reuse; use this when you need to check the result yourself. On the hosted connector gemini_view_media does the same for an image you already have"),
62033
62508
  seed: external_exports.number().int().optional().describe("Seed for reproducible generation; random if omitted"),
62034
62509
  thinking_level: external_exports.enum(["minimal", "high"]).optional().describe("Reasoning depth (Gemini 3 models); higher can help complex/structural edits"),
@@ -62482,13 +62957,13 @@ function persistableResult(result) {
62482
62957
  return { result };
62483
62958
  }
62484
62959
  function awaitWithBudget(promise2, waitMs) {
62485
- return new Promise((resolve5, reject) => {
62486
- const timer = setTimeout(() => resolve5(void 0), waitMs);
62960
+ return new Promise((resolve6, reject) => {
62961
+ const timer = setTimeout(() => resolve6(void 0), waitMs);
62487
62962
  timer.unref?.();
62488
62963
  promise2.then(
62489
62964
  (result) => {
62490
62965
  clearTimeout(timer);
62491
- resolve5(result);
62966
+ resolve6(result);
62492
62967
  },
62493
62968
  (err) => {
62494
62969
  clearTimeout(timer);
@@ -62556,7 +63031,7 @@ var JobRegistry = class {
62556
63031
  /** Await outstanding durable writes. Tests only — handlers never need it. */
62557
63032
  async drain() {
62558
63033
  for (let round = 0; round < 50; round++) {
62559
- await new Promise((resolve5) => setTimeout(resolve5, 0));
63034
+ await new Promise((resolve6) => setTimeout(resolve6, 0));
62560
63035
  if (!this.pending.size) return;
62561
63036
  await Promise.allSettled([...this.pending]);
62562
63037
  }
@@ -62988,10 +63463,18 @@ async function confirmWrite(ctx, o) {
62988
63463
  message: o.message,
62989
63464
  details: preview,
62990
63465
  tool: o.tool,
63466
+ // One API key per process (a hosted deployment runs one child per
63467
+ // account), so there is no in-process principal to bind.
63468
+ account: void 0,
63469
+ args: withoutConfirmToken(o.args),
62991
63470
  confirmToken: o.confirmToken,
62992
63471
  subject: () => ({ target: o.target, payload: o.payload, preview })
62993
63472
  }));
62994
63473
  }
63474
+ function withoutConfirmToken(args) {
63475
+ const { confirmToken: _omit, ...rest } = args;
63476
+ return rest;
63477
+ }
62995
63478
  async function confirmLocalInputs(ctx, o) {
62996
63479
  const hasLocal = (o.imagePaths?.length ?? 0) > 0 || Boolean(o.videoPath);
62997
63480
  if (!hasLocal) return void 0;
@@ -63008,6 +63491,7 @@ async function confirmLocalInputs(ctx, o) {
63008
63491
  body: { inputs },
63009
63492
  target: "",
63010
63493
  payload: { request: o.request, inputs },
63494
+ args: o.request,
63011
63495
  confirmToken: o.confirmToken
63012
63496
  });
63013
63497
  }
@@ -63021,7 +63505,7 @@ function registerGenerateTools(server, client2) {
63021
63505
  "gemini_image_generate",
63022
63506
  {
63023
63507
  description: "Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro). If the result will likely be refined iteratively, prefer gemini_interact (multi-turn) as the entry point. " + confirmNote("Local file inputs are confirmed first"),
63024
- annotations: { readOnlyHint: false, openWorldHint: true },
63508
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
63025
63509
  inputSchema: external_exports.object({
63026
63510
  prompt: external_exports.string().min(1).describe("Text prompt describing the image"),
63027
63511
  count: external_exports.number().int().positive().max(8).optional().describe("Number of independent images (default 1)"),
@@ -63141,7 +63625,7 @@ function registerGenerateTools(server, client2) {
63141
63625
  "gemini_image_edit",
63142
63626
  {
63143
63627
  description: "Edit or compose images: provide one or more input images (paths or base64), plus a text instruction. For a SERIES of successive edits to the same image, prefer gemini_interact (multi-turn) \u2014 it keeps edit context and avoids re-processing the full image each round; use gemini_image_edit for one-off edits or composing multiple distinct inputs. Gemini over-preserves the input; there is no edit-strength control \u2014 for large structural changes, reroll with a different `seed` or more forceful wording. " + confirmNote("Local file inputs are confirmed first"),
63144
- annotations: { readOnlyHint: false, openWorldHint: true },
63628
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
63145
63629
  inputSchema: external_exports.object({
63146
63630
  prompt: external_exports.string().min(1).describe("Instruction describing the edit or composition"),
63147
63631
  images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to input image file(s) (1 = edit, 2+ = compose). Local paths must be inside GEMINI_REFERENCE_DIR (else GEMINI_UPLOAD_DIR) when set."),
@@ -63232,7 +63716,7 @@ function registerSetTools(server, client2) {
63232
63716
  "gemini_image_set",
63233
63717
  {
63234
63718
  description: 'Generate a consistent SET of images: a master image from master_prompt, then one image per scene that references the master so the subject/style stays consistent. Provide `scenes` (explicit per-image prompts) OR `count` (variations of the master). Scene generations run in parallel (reference_mode "master", the default). On the hosted connector: saved `characters` and a saved `style` can seed the whole set by name, multi-image results include a `bundle_url` zip of every image (one curl instead of N), and `max_wait_ms` returns a pollable job handle if the batch runs long. ' + confirmNote("Local file inputs are confirmed first"),
63235
- annotations: { readOnlyHint: false, openWorldHint: true },
63719
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
63236
63720
  inputSchema: external_exports.object({
63237
63721
  master_prompt: external_exports.string().min(1).describe("Prompt for the master/reference image"),
63238
63722
  scenes: external_exports.array(external_exports.string().min(1)).min(1).max(8).optional().describe("Per-image prompts (1-8); each references the master"),
@@ -63391,6 +63875,7 @@ var LIBRARY_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,39}$/;
63391
63875
  function isValidLibraryName(name) {
63392
63876
  return LIBRARY_NAME_PATTERN.test(name);
63393
63877
  }
63878
+ var LIBRARY_MAX_LIST_PAGES = 20;
63394
63879
  function createR2Library(bucket, opts) {
63395
63880
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
63396
63881
  const tenantId = tenantResolver(opts.tenant);
@@ -63422,10 +63907,10 @@ function createR2Library(bucket, opts) {
63422
63907
  const prefix = `${LIBRARY_KEY_PREFIX}/${await tenantId()}/${kind}/`;
63423
63908
  const keys = [];
63424
63909
  let cursor;
63425
- for (; ; ) {
63910
+ for (let n = 0; n < LIBRARY_MAX_LIST_PAGES; n++) {
63426
63911
  const page = await bucket.list({ prefix, cursor, limit: 1e3 });
63427
63912
  keys.push(...page.objects.map((o) => o.key).filter((k) => k.endsWith(".json")));
63428
- if (!page.truncated) break;
63913
+ if (!page.truncated || !page.cursor) break;
63429
63914
  cursor = page.cursor;
63430
63915
  }
63431
63916
  const records = await Promise.all(keys.map((k) => readJson(k)));
@@ -63713,8 +64198,9 @@ var SessionState = class {
63713
64198
  * Accumulated in the CLIENT rather than in the tools, so a call that was
63714
64199
  * billed and then failed (a safety filter returning no image, say) still
63715
64200
  * counts — it cost money regardless of whether a tool could use the result.
63716
- * Per session, never module scope: one hosted process serves many tenants,
63717
- * and a shared total would report other people's spend as yours.
64201
+ * On the session, never module scope: a module-level total would report
64202
+ * other users' spend as yours on any host that served several users from one
64203
+ * process (see the invariant above).
63718
64204
  */
63719
64205
  usageTotal;
63720
64206
  /** How many upstream calls contributed to {@link usageTotal}. */
@@ -63779,6 +64265,25 @@ function resolveTimeoutMs(perCallMs, imageSize) {
63779
64265
  return imageSize === "4K" ? REQUEST_TIMEOUT_4K_MS : REQUEST_TIMEOUT_MS;
63780
64266
  }
63781
64267
  var UPLOAD_BASE_URL = "https://generativelanguage.googleapis.com/upload/v1beta";
64268
+ function isGoogleUploadUrl(raw) {
64269
+ let u;
64270
+ try {
64271
+ u = new URL(raw);
64272
+ } catch {
64273
+ return false;
64274
+ }
64275
+ return u.protocol === "https:" && (u.hostname === "googleapis.com" || u.hostname.endsWith(".googleapis.com"));
64276
+ }
64277
+ var MIN_TRANSFER_BYTES_PER_SEC = 256 * 1024;
64278
+ function transferTimeoutMs(bytes) {
64279
+ return resolveTimeoutMs() + Math.ceil(bytes / MIN_TRANSFER_BYTES_PER_SEC * 1e3);
64280
+ }
64281
+ function transferDeadline(ms) {
64282
+ const ctrl = new AbortController();
64283
+ const timer = setTimeout(() => ctrl.abort(new DOMException(`timed out after ${ms}ms`, "TimeoutError")), ms);
64284
+ timer.unref?.();
64285
+ return { signal: ctrl.signal, clear: () => clearTimeout(timer) };
64286
+ }
63782
64287
  var FILE_MAX_BYTES = 2 * 1024 ** 3;
63783
64288
  var FILE_POLL_INTERVAL_MS = 2e3;
63784
64289
  var FILE_POLL_MAX_ATTEMPTS = 150;
@@ -63855,10 +64360,9 @@ var GeminiClient = class {
63855
64360
  mediaSink;
63856
64361
  /**
63857
64362
  * This client's session memory — job registry, last-interaction id, written
63858
- * outputs. One client is one session: the hosted connector builds a client
63859
- * per authenticated user, so hanging the state here is what keeps it from
63860
- * leaking between tenants sharing an isolate (see src/session.ts). The stdio
63861
- * server is single-user, so its one singleton client is one session.
64363
+ * outputs. One client is one session. Production builds one client per
64364
+ * process and one process is one user, so this is process-wide — see the
64365
+ * invariant (and what a multi-user host would need) in src/session.ts.
63862
64366
  */
63863
64367
  session = new SessionState();
63864
64368
  /** Per-account character/style library — hosted connector only (see options). */
@@ -63991,8 +64495,9 @@ var GeminiClient = class {
63991
64495
  * These two upload calls are raw `fetchImpl` (not `createApiClient`): step 1
63992
64496
  * needs the response *header* and step 2 posts a binary body — neither fits
63993
64497
  * `fetchJson`. The poll DOES go through the shared client (timeout + retry).
63994
- * Uploads deliberately have no abort timeout: a multi-hundred-MB video can
63995
- * legitimately take longer than any fixed budget.
64498
+ * The deadline scales with the file (see `transferTimeoutMs`): a
64499
+ * multi-hundred-MB video can legitimately take longer than any fixed budget,
64500
+ * but a stalled upload must still fail rather than hang.
63996
64501
  *
63997
64502
  * Returned files live ~48h (`expirationTime`); per-file cap is 2 GB
63998
64503
  * (enforced locally before any bytes are sent).
@@ -64058,12 +64563,32 @@ var GeminiClient = class {
64058
64563
  * These two upload calls are raw `fetchImpl` (not `createApiClient`): step 1
64059
64564
  * needs the response *header* and step 2 posts a binary body — neither fits
64060
64565
  * `fetchJson`. The poll DOES go through the shared client (timeout + retry).
64061
- * Uploads carry no abort timeout by default: a multi-hundred-MB file can
64062
- * legitimately take longer than any fixed budget. A caller that is uploading
64063
- * OPPORTUNISTICALLY passes its own `signal` (see `uploadBytes`).
64566
+ * By default the two round trips get a deadline sized to the file (see
64567
+ * `transferTimeoutMs`) — a multi-hundred-MB file can legitimately take longer
64568
+ * than any fixed budget, but a stalled one must fail rather than hang. A
64569
+ * caller that is uploading OPPORTUNISTICALLY passes its own, shorter
64570
+ * `signal` instead (see `uploadBytes`).
64064
64571
  */
64065
64572
  async uploadToFilesApi(body, mimeType, displayName, contentLength, signal) {
64066
64573
  const key = this.requireKey();
64574
+ const deadline = signal ? void 0 : transferDeadline(transferTimeoutMs(contentLength));
64575
+ let uploaded;
64576
+ try {
64577
+ uploaded = await this.sendToFilesApi(body, mimeType, displayName, contentLength, key, signal ?? deadline.signal);
64578
+ } catch (err) {
64579
+ if (deadline?.signal.aborted) {
64580
+ throw new McpToolError(`${SERVICE} Files API upload of ${displayName} timed out after ${transferTimeoutMs(contentLength)}ms`, {
64581
+ hint: "The upload stalled. Retry, or check the network between this server and generativelanguage.googleapis.com."
64582
+ });
64583
+ }
64584
+ throw err;
64585
+ } finally {
64586
+ deadline?.clear();
64587
+ }
64588
+ return this.awaitFileActive(uploaded.name, uploaded.file, mimeType);
64589
+ }
64590
+ /** {@link uploadToFilesApi}'s steps 1 and 2 (start + upload/finalize), under `signal`. */
64591
+ async sendToFilesApi(body, mimeType, displayName, contentLength, key, signal) {
64067
64592
  const doFetch = this.fetchImpl;
64068
64593
  const startPath = "/files";
64069
64594
  const startRes = await doFetch(`${UPLOAD_BASE_URL}${startPath}`, {
@@ -64077,7 +64602,7 @@ var GeminiClient = class {
64077
64602
  "Content-Type": "application/json"
64078
64603
  },
64079
64604
  body: JSON.stringify({ file: { display_name: displayName } }),
64080
- ...signal ? { signal } : {}
64605
+ signal
64081
64606
  });
64082
64607
  if (!startRes.ok) {
64083
64608
  throw new McpToolError(
@@ -64090,6 +64615,11 @@ var GeminiClient = class {
64090
64615
  hint: "The Files API resumable-upload contract may have changed \u2014 see docs/GEMINI-API.md."
64091
64616
  });
64092
64617
  }
64618
+ if (!isGoogleUploadUrl(uploadUrl)) {
64619
+ throw new McpToolError(`${SERVICE} upload start returned a session URL outside googleapis.com; refusing to send the file`, {
64620
+ hint: "The Files API resumable-upload contract may have changed \u2014 see docs/GEMINI-API.md."
64621
+ });
64622
+ }
64093
64623
  const upRes = await doFetch(uploadUrl, {
64094
64624
  method: "POST",
64095
64625
  headers: {
@@ -64097,7 +64627,7 @@ var GeminiClient = class {
64097
64627
  "X-Goog-Upload-Command": "upload, finalize"
64098
64628
  },
64099
64629
  body,
64100
- ...signal ? { signal } : {}
64630
+ signal
64101
64631
  });
64102
64632
  if (!upRes.ok) {
64103
64633
  throw new McpToolError(
@@ -64109,6 +64639,10 @@ var GeminiClient = class {
64109
64639
  if (!/^files\/[\w-]+$/.test(name)) {
64110
64640
  throw new McpToolError(`${SERVICE} upload returned an unexpected file name: ${name || "(none)"}`);
64111
64641
  }
64642
+ return { name, file: file2 };
64643
+ }
64644
+ /** {@link uploadToFilesApi}'s step 3: wait out PROCESSING. Outside the upload deadline. */
64645
+ async awaitFileActive(name, file2, mimeType) {
64112
64646
  for (let attempt = 0; file2.state === "PROCESSING"; attempt++) {
64113
64647
  if (attempt >= FILE_POLL_MAX_ATTEMPTS) {
64114
64648
  throw new McpToolError(`${SERVICE} file ${name} is still processing after ${FILE_POLL_INTERVAL_MS * FILE_POLL_MAX_ATTEMPTS / 6e4} minutes`, {
@@ -64546,32 +65080,46 @@ var GeminiClient = class {
64546
65080
  let current = requested;
64547
65081
  for (let hop = 0; ; hop++) {
64548
65082
  const url4 = assertMediaHost(current, requested);
64549
- const res = await doFetch(url4.toString(), {
64550
- redirect: "manual",
64551
- headers: { "x-goog-api-key": key }
64552
- });
64553
- if (res.status >= 300 && res.status < 400) {
64554
- const location = res.headers?.get("location");
64555
- if (!location) {
64556
- throw new McpToolError(`${SERVICE} media download returned HTTP ${res.status} with no Location header for ${current}`);
65083
+ const budgetMs = transferTimeoutMs(MEDIA_DOWNLOAD_MAX_BYTES);
65084
+ const deadline = transferDeadline(budgetMs);
65085
+ try {
65086
+ const res = await doFetch(url4.toString(), {
65087
+ redirect: "manual",
65088
+ headers: { "x-goog-api-key": key },
65089
+ signal: deadline.signal
65090
+ });
65091
+ if (res.status >= 300 && res.status < 400) {
65092
+ const location = res.headers?.get("location");
65093
+ if (!location) {
65094
+ throw new McpToolError(`${SERVICE} media download returned HTTP ${res.status} with no Location header for ${current}`);
65095
+ }
65096
+ if (hop >= MAX_REDIRECTS) {
65097
+ throw new McpToolError(`${SERVICE} media download for ${requested} exceeded ${MAX_REDIRECTS} redirects (last hop: ${location}).`);
65098
+ }
65099
+ current = new URL(location, url4).toString();
65100
+ continue;
65101
+ }
65102
+ if (!res.ok) {
65103
+ throw new McpToolError(
65104
+ `${SERVICE} media download failed (HTTP ${res.status}) for ${current}`,
65105
+ { hint: "Generated files live in the Files API for ~48h and the download requires the api key that created them." }
65106
+ );
64557
65107
  }
64558
- if (hop >= MAX_REDIRECTS) {
64559
- throw new McpToolError(`${SERVICE} media download for ${requested} exceeded ${MAX_REDIRECTS} redirects (last hop: ${location}).`);
65108
+ const bytes = await readCapped(res, MEDIA_DOWNLOAD_MAX_BYTES, current, requested, MEDIA_CAP_SUBJECT);
65109
+ return {
65110
+ base64: bytesToBase64(bytes),
65111
+ mimeType: media.mimeType || res.headers?.get("content-type") || "application/octet-stream"
65112
+ };
65113
+ } catch (err) {
65114
+ if (deadline.signal.aborted && !(err instanceof McpToolError)) {
65115
+ throw new McpToolError(`${SERVICE} media download timed out after ${budgetMs}ms for ${current}`, {
65116
+ hint: "The download stalled. Recover the result with gemini_get_result, or retry."
65117
+ });
64560
65118
  }
64561
- current = new URL(location, url4).toString();
64562
- continue;
64563
- }
64564
- if (!res.ok) {
64565
- throw new McpToolError(
64566
- `${SERVICE} media download failed (HTTP ${res.status}) for ${current}`,
64567
- { hint: "Generated files live in the Files API for ~48h and the download requires the api key that created them." }
64568
- );
65119
+ throw err;
65120
+ } finally {
65121
+ deadline.clear();
64569
65122
  }
64570
- const bytes = await readCapped(res, MEDIA_DOWNLOAD_MAX_BYTES, current, requested, MEDIA_CAP_SUBJECT);
64571
- return {
64572
- base64: bytesToBase64(bytes),
64573
- mimeType: media.mimeType || res.headers?.get("content-type") || "application/octet-stream"
64574
- };
64575
65123
  }
64576
65124
  }
64577
65125
  /** {@link downloadMedia} over a list, preserving order. */
@@ -64651,7 +65199,10 @@ function isDeliveryUnsupported(err) {
64651
65199
  }
64652
65200
  function hostedStorage(blob = blobStoreFromEnv()) {
64653
65201
  if (!blob) return {};
64654
- const tenant = pinnedTenant(blob.bucket, () => tenantIdFor(readEnvVar("GEMINI_API_KEY") ?? "local"));
65202
+ const tenant = pinnedTenant(blob.bucket, async () => {
65203
+ const key = readEnvVar("GEMINI_API_KEY");
65204
+ return key ? tenantIdFor(key) : { tenant: await tenantIdFor("local"), pin: false };
65205
+ });
64655
65206
  return {
64656
65207
  mediaSink: createR2Sink(blob.bucket, {
64657
65208
  // The SAME `links` object the minter gets below. Media GETs and upload
@@ -64690,10 +65241,17 @@ var client = new GeminiClient(hostedStorage());
64690
65241
  init_images();
64691
65242
 
64692
65243
  // src/sidecar.ts
64693
- import { readdir, readFile as readFile3, stat as stat2 } from "node:fs/promises";
64694
- import { existsSync as existsSync4 } from "node:fs";
64695
- import { join as join8 } from "node:path";
65244
+ import { readdir, readFile as readFile3, stat as stat2, lstat } from "node:fs/promises";
65245
+ import { join as join9, resolve as resolve5 } from "node:path";
64696
65246
  var SIDECAR_SUFFIX2 = ".json";
65247
+ var SIDECAR_NAME = /^.+\.[A-Za-z0-9]+\.json$/;
65248
+ async function isRegularFile(path) {
65249
+ try {
65250
+ return (await lstat(path)).isFile();
65251
+ } catch {
65252
+ return false;
65253
+ }
65254
+ }
64697
65255
  async function readSidecars(dir) {
64698
65256
  let entries;
64699
65257
  try {
@@ -64701,20 +65259,25 @@ async function readSidecars(dir) {
64701
65259
  } catch {
64702
65260
  return [];
64703
65261
  }
65262
+ const sidecarNames = entries.filter((e) => SIDECAR_NAME.test(e));
65263
+ const outputs = new Set(sidecarNames.map((e) => resolve5(dir, e.slice(0, -SIDECAR_SUFFIX2.length))));
64704
65264
  const records = [];
64705
- for (const entry of entries) {
64706
- if (!entry.endsWith(SIDECAR_SUFFIX2)) continue;
64707
- const sidecarPath = join8(dir, entry);
65265
+ for (const entry of sidecarNames) {
65266
+ const sidecarPath = join9(dir, entry);
64708
65267
  try {
64709
65268
  const parsed = JSON.parse(await readFile3(sidecarPath, "utf8"));
64710
65269
  if (typeof parsed !== "object" || parsed === null) continue;
64711
65270
  const { interaction_id: interactionId, images } = parsed;
64712
65271
  if (typeof interactionId !== "string" || !interactionId) continue;
64713
65272
  const paths = Array.isArray(images) ? images.filter((p) => typeof p === "string") : [];
65273
+ const kept = [];
65274
+ for (const p of paths) {
65275
+ if (outputs.has(resolve5(p)) && await isRegularFile(p)) kept.push(p);
65276
+ }
64714
65277
  records.push({
64715
65278
  sidecarPath,
64716
65279
  interactionId,
64717
- images: paths.filter((p) => existsSync4(p)),
65280
+ images: kept,
64718
65281
  mtimeMs: (await stat2(sidecarPath)).mtimeMs
64719
65282
  });
64720
65283
  } catch {
@@ -64770,7 +65333,7 @@ function registerInteractTools(server, client2) {
64770
65333
  "gemini_interact",
64771
65334
  {
64772
65335
  description: "Preferred tool for iterative refinement of a single image \u2014 multi-turn generation and editing via the Interactions API. To refine, pass the returned interaction `id` as `previous_interaction_id` on the next call; do NOT start a new interaction or re-send the image for each tweak. `continue_last: true` chains from this session's most recent interaction without threading the id. " + recoveryDescription + "Output is JPEG. " + confirmNote("Local file inputs are confirmed first"),
64773
- annotations: { readOnlyHint: false, openWorldHint: true },
65336
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
64774
65337
  inputSchema: external_exports.object({
64775
65338
  input: external_exports.string().min(1).describe("Text prompt or editing instruction"),
64776
65339
  previous_interaction_id: external_exports.string().optional().describe("ID from a prior gemini_interact call \u2014 continues that multi-turn conversation"),
@@ -64785,7 +65348,7 @@ function registerInteractTools(server, client2) {
64785
65348
  image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
64786
65349
  thinking_level: external_exports.enum(["minimal", "high"]).optional().describe("Reasoning depth; higher can help complex/structural edits"),
64787
65350
  filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified input)"),
64788
- output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
65351
+ output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or ~/Downloads/gemini-mcp)"),
64789
65352
  inline: external_exports.boolean().optional().describe("Return base64 images inline instead of writing to disk"),
64790
65353
  google_search: external_exports.boolean().optional().describe("Ground the image in live Google Search results (current events, weather, data)"),
64791
65354
  search_types: external_exports.array(external_exports.enum(["web_search", "image_search"])).optional().describe(
@@ -64943,7 +65506,7 @@ function registerVideoTools(server, client2) {
64943
65506
  "gemini_video_generate",
64944
65507
  {
64945
65508
  description: 'Generate a short video (~10s) via the Gemini omni model: text\u2192video, image\u2192video / reference\u2192video (supply reference image[s]), interpolate between two stills (pass first frame then last frame as images), or continue a prior video (task: "edit" or "extend" + previous_interaction_id / continue_last; extensions add ~3-10s each, to ~40s total). Cost scales with `resolution` \u2014 draft at 360p, keep at 1080p/4k. Output is written to disk as MP4 (video has no inline MCP block). Video runs long \u2014 give it a `max_wait_ms` budget (or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account. ' + confirmNote("Local file inputs are confirmed first"),
64946
- annotations: { readOnlyHint: false, openWorldHint: true },
65509
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
64947
65510
  inputSchema: external_exports.object({
64948
65511
  prompt: external_exports.string().min(1).describe("Description of the video to generate (or the edit instruction when task=edit)"),
64949
65512
  aspect_ratio: external_exports.enum(VIDEO_ASPECT_RATIOS).optional().describe("Exact output aspect ratio (omni: 16:9 or 9:16). `orientation` is the plain-language shorthand; this wins if both are given."),
@@ -64956,7 +65519,7 @@ function registerVideoTools(server, client2) {
64956
65519
  images_base64: imagesBase64Schema(),
64957
65520
  from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS clipboard as a reference"),
64958
65521
  filename: external_exports.string().optional().describe("Base filename for the output video (extension stripped; default: slugified prompt)"),
64959
- output_dir: external_exports.string().optional().describe("Directory to write the video to (default: $GEMINI_OUTPUT_DIR or cwd)"),
65522
+ output_dir: external_exports.string().optional().describe("Directory to write the video to (default: $GEMINI_OUTPUT_DIR or ~/Downloads/gemini-mcp)"),
64960
65523
  model: external_exports.string().regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")').optional().describe(`Model id override (default: ${DEFAULT_VIDEO_MODEL})`),
64961
65524
  delivery: external_exports.enum(VIDEO_DELIVERY).optional().describe('How the clip comes back: "uri" (default \u2014 a Files API link the server downloads, no size ceiling) or "inline" (base64, capped ~4MB)'),
64962
65525
  background: external_exports.boolean().optional().describe("Run the generation on Google's side and poll it, so a killed job can be recovered by gemini_get_result. Trade-off: retrieving a backgrounded interaction is unreliable today (some become permanently unreadable), so the default is off"),
@@ -65055,7 +65618,7 @@ function registerMusicTools(server, client2) {
65055
65618
  "gemini_music_generate",
65056
65619
  {
65057
65620
  description: "Generate music from a text prompt (mood, genre, instruments, structure, or lyrics inline) via a Lyria model: lyria-3-clip-preview (30s instrumental clip, default, cheapest), lyria-3.5 (full-length song with vocals) or lyria-3-pro-preview (longer-form). Output is MP3, written to disk (or returned inline). Single-turn: a track cannot be refined by a follow-up call, so put the whole brief in the prompt. Runs long \u2014 give it a `max_wait_ms` budget (or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account. " + confirmNote("Local file inputs are confirmed first"),
65058
- annotations: { readOnlyHint: false, openWorldHint: true },
65621
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
65059
65622
  inputSchema: external_exports.object({
65060
65623
  prompt: external_exports.string().min(1).describe("Description of the music: mood, genre, instruments, tempo, structure, or lyrics"),
65061
65624
  model: external_exports.enum(MUSIC_MODELS).optional().describe(`Lyria model (default: ${DEFAULT_MUSIC_MODEL} \u2014 30s, $0.04). lyria-3.5 and lyria-3-pro-preview run minutes-long at $0.08.`),
@@ -65065,7 +65628,7 @@ function registerMusicTools(server, client2) {
65065
65628
  images_base64: imagesBase64Schema(),
65066
65629
  from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS clipboard as a reference"),
65067
65630
  filename: external_exports.string().optional().describe("Base filename for the output audio (extension stripped; default: slugified prompt)"),
65068
- output_dir: external_exports.string().optional().describe("Directory to write audio to (default: $GEMINI_OUTPUT_DIR or cwd)"),
65631
+ output_dir: external_exports.string().optional().describe("Directory to write audio to (default: $GEMINI_OUTPUT_DIR or ~/Downloads/gemini-mcp)"),
65069
65632
  inline: external_exports.boolean().optional().describe("Return base64 audio inline instead of writing to disk. A 30s track is ~1.9MB of base64 \u2014 the default hands back a path instead"),
65070
65633
  background: external_exports.boolean().optional().describe("Run the generation on Google's side and poll it, so a killed job can be recovered by gemini_get_result. Off by default \u2014 see gemini_video_generate"),
65071
65634
  timeout_ms: timeoutMsSchema,
@@ -65137,10 +65700,10 @@ function registerJobTools(server, client2) {
65137
65700
  description: 'Retrieve a generation started with `async: true` or handed off by `max_wait_ms`. Pass the returned `job_id`: while running it reports status "running"; on completion it returns the normal result (image URLs/paths + meta); on failure it raises the recorded error, including the case where the generation was killed before it finished. On the hosted connector job records are stored durably and survive a restart; on a local stdio server they live with the process and expire ~10 min after completion, where the output dir / <image>.json sidecar is the fallback. A killed video/music job started with `background: true` is recovered from its upstream interaction when it finished there.',
65138
65701
  // NOT read-only any more: recovering a killed job writes the media it
65139
65702
  // pulls back, exactly as the generation tool would have.
65140
- annotations: { readOnlyHint: false, openWorldHint: true },
65703
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
65141
65704
  inputSchema: external_exports.object({
65142
65705
  job_id: external_exports.string().min(1).describe("The job_id returned by a generation tool called with async: true"),
65143
- output_dir: external_exports.string().optional().describe("Where to write media recovered from a killed job (default: $GEMINI_OUTPUT_DIR or cwd)")
65706
+ output_dir: external_exports.string().optional().describe("Where to write media recovered from a killed job (default: $GEMINI_OUTPUT_DIR or ~/Downloads/gemini-mcp)")
65144
65707
  })
65145
65708
  },
65146
65709
  async (args) => {
@@ -65215,7 +65778,7 @@ function registerUsageTools(server, client2) {
65215
65778
  "gemini_token_usage",
65216
65779
  {
65217
65780
  description: "Token usage for this session so far \u2014 what every generation has cost in tokens, added up. Call it before and after a workflow and subtract to get that workflow's cost; call it after a single generation for that call's. Reports tokens AND an estimated USD cost, priced per call against each call's own model and stamped with the date its rates were read (override with GEMINI_RATE_CARD). Note there is no account-balance endpoint to query \u2014 Google Cloud is post-paid and its billing data lags by hours \u2014 so this is the accurate way to attribute spend to a call.",
65218
- annotations: toolAnnotations({ readOnly: true }),
65781
+ annotations: toolAnnotations({ readOnly: true, openWorld: false }),
65219
65782
  inputSchema: external_exports.object({
65220
65783
  reset: external_exports.boolean().optional().describe("Zero the running total after reporting it, so the next call measures from here. Use it to bracket a workflow without arithmetic.")
65221
65784
  })
@@ -65276,7 +65839,7 @@ function registerFileTools(server, client2) {
65276
65839
  "gemini_upload_file",
65277
65840
  {
65278
65841
  description: "Upload a file \u2014 an image, reference photo, picture, screenshot, video or audio clip \u2014 to the Gemini Files API ONCE, and get back a reusable `file_uri` (`files/<id>`) to attach to later image, video or music generations. Keywords: upload, upload file, upload image, upload photo, attach, reference image, reference photo, file_uri, files api, image reference, reuse across calls. Use this instead of pasting base64 into a tool call: the reference is a short string, so no image bytes ever enter the conversation, and it can be reused across many generations until it expires (~48h). Provide exactly one of `url` (the server downloads it), `data_base64`" + (onDisk ? ", or `path` (a local file)." : ", or `r2_key` (re-upload media this connector generated, from media[].r2_key in an earlier result).") + (onDisk ? " " + confirmNote("A local `path` is confirmed first") : " To upload a local file without base64: mint a signed PUT URL with gemini_get_upload_url, PUT the bytes to it, then pass the returned r2_key here."),
65279
- annotations: { readOnlyHint: false, openWorldHint: true },
65842
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
65280
65843
  inputSchema: external_exports.object({
65281
65844
  url: external_exports.string().url().optional().describe(
65282
65845
  `Public https URL the SERVER downloads and uploads (image/video/audio, up to ${wholeMb(urlMaxBytes)}MB). No bytes pass through the conversation.` + (onDisk ? "" : " Larger files: PUT them to a gemini_get_upload_url link and pass the r2_key instead \u2014 that path does not buffer.")
@@ -65329,6 +65892,7 @@ function registerFileTools(server, client2) {
65329
65892
  body: { inputs },
65330
65893
  target: resolved,
65331
65894
  payload: { inputs, display_name: args.display_name },
65895
+ args,
65332
65896
  confirmToken: args.confirmToken
65333
65897
  });
65334
65898
  if (gate) return gate;
@@ -65519,6 +66083,7 @@ function registerFileTools(server, client2) {
65519
66083
  path: `/v1beta/${args.file_uri}`,
65520
66084
  target: args.file_uri,
65521
66085
  payload: { file_uri: args.file_uri },
66086
+ args,
65522
66087
  confirmToken: args.confirmToken
65523
66088
  });
65524
66089
  if (gate) return gate;
@@ -65605,7 +66170,7 @@ function registerLibraryTools(server, client2) {
65605
66170
  "gemini_save_character",
65606
66171
  {
65607
66172
  description: "Save a named character to this account's persistent reference library: one reference image plus a short description. Saved characters have NO expiry \u2014 unlike uploads (~retention window) and Files API references (~48h) \u2014 so a name saved once keeps working in every later session. Re-saving a name replaces it. " + USE_CHARACTERS_HINT,
65608
- annotations: { readOnlyHint: false, openWorldHint: false },
66173
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
65609
66174
  inputSchema: external_exports.object({
65610
66175
  name: NAME_FIELD,
65611
66176
  description: external_exports.string().min(1).describe("Short prose description woven into generation prompts (age, look, defining features, species\u2026)"),
@@ -65670,6 +66235,7 @@ function registerLibraryTools(server, client2) {
65670
66235
  path: `characters/${args.name}`,
65671
66236
  target: args.name,
65672
66237
  payload: { name: args.name },
66238
+ args,
65673
66239
  confirmToken: args.confirmToken
65674
66240
  });
65675
66241
  if (gate) return gate;
@@ -65684,7 +66250,7 @@ function registerLibraryTools(server, client2) {
65684
66250
  "gemini_save_style",
65685
66251
  {
65686
66252
  description: "Save a named style preset to this account's persistent reference library: a reusable prompt fragment (and optionally a reference image) that a single word then applies to any generation. No expiry; re-saving a name replaces it. " + USE_STYLE_HINT,
65687
- annotations: { readOnlyHint: false, openWorldHint: false },
66253
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
65688
66254
  inputSchema: external_exports.object({
65689
66255
  name: NAME_FIELD,
65690
66256
  prompt_fragment: external_exports.string().min(1).describe('The style text appended to generation prompts, e.g. "bold cartoon style, thick outlines, saturated colors"'),
@@ -65749,6 +66315,7 @@ function registerLibraryTools(server, client2) {
65749
66315
  path: `styles/${args.name}`,
65750
66316
  target: args.name,
65751
66317
  payload: { name: args.name },
66318
+ args,
65752
66319
  confirmToken: args.confirmToken
65753
66320
  });
65754
66321
  if (gate) return gate;