@chrischall/gemini-mcp 2.1.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -69,10 +69,37 @@ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
69
69
  function withCallerCapabilities(capabilities, fn) {
70
70
  return capabilities ? storage2.run(capabilities, fn) : fn();
71
71
  }
72
- var storage2;
72
+ function currentCallerCapabilities() {
73
+ return storage2.getStore();
74
+ }
75
+ function isRecord(value) {
76
+ return typeof value === "object" && value !== null && !Array.isArray(value);
77
+ }
78
+ function callerCapabilities(ctx) {
79
+ const envelope = isRecord(ctx) && isRecord(ctx.mcpReq) ? ctx.mcpReq.envelope : void 0;
80
+ if (isRecord(envelope)) {
81
+ const declared = envelope[ENVELOPE_CAPABILITIES_KEY];
82
+ if (isRecord(declared))
83
+ return declared;
84
+ }
85
+ return currentCallerCapabilities();
86
+ }
87
+ function callerAcceptsFormElicitation(ctx) {
88
+ const capabilities = callerCapabilities(ctx);
89
+ if (!capabilities)
90
+ return void 0;
91
+ const elicitation = capabilities.elicitation;
92
+ if (!isRecord(elicitation))
93
+ return false;
94
+ const namedModes = ELICITATION_MODES.filter((mode) => mode in elicitation);
95
+ return namedModes.length === 0 || namedModes.includes("form");
96
+ }
97
+ var storage2, ENVELOPE_CAPABILITIES_KEY, ELICITATION_MODES;
73
98
  var init_caller = __esm({
74
99
  "node_modules/@chrischall/mcp-utils/dist/caller/index.js"() {
75
100
  storage2 = new AsyncLocalStorage2();
101
+ ENVELOPE_CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
102
+ ELICITATION_MODES = ["form", "url"];
76
103
  }
77
104
  });
78
105
 
@@ -26876,6 +26903,37 @@ function buildInputRequired(spec) {
26876
26903
  ...spec.requestState !== void 0 && { requestState: spec.requestState }
26877
26904
  };
26878
26905
  }
26906
+ function acceptedContent(responses, key, schema) {
26907
+ const view = inputResponse(responses, key);
26908
+ if (view.kind !== "elicit" || view.action !== "accept" || view.content === void 0) return void 0;
26909
+ if (schema === void 0) return view.content;
26910
+ const outcome = schema["~standard"].validate(view.content);
26911
+ if (outcome instanceof Promise) throw new TypeError("acceptedContent(responses, key, schema) requires a synchronously-validating schema");
26912
+ return outcome.issues === void 0 ? outcome.value : void 0;
26913
+ }
26914
+ function inputResponse(responses, key) {
26915
+ if (responses === void 0 || typeof responses !== "object" || responses === null) return { kind: "missing" };
26916
+ const entry = responses[key];
26917
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return { kind: "missing" };
26918
+ const candidate = entry;
26919
+ if (candidate["action"] === "accept" || candidate["action"] === "decline" || candidate["action"] === "cancel") {
26920
+ const content = candidate["content"];
26921
+ return {
26922
+ kind: "elicit",
26923
+ action: candidate["action"],
26924
+ ...content !== null && typeof content === "object" && !Array.isArray(content) && { content }
26925
+ };
26926
+ }
26927
+ if (Array.isArray(candidate["roots"])) return {
26928
+ kind: "roots",
26929
+ roots: candidate["roots"]
26930
+ };
26931
+ if (typeof candidate["role"] === "string" && candidate["content"] !== void 0) return {
26932
+ kind: "sampling",
26933
+ result: candidate
26934
+ };
26935
+ return { kind: "missing" };
26936
+ }
26879
26937
  function inputRequiredRoundsExceededMessage(method, maxRounds) {
26880
26938
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
26881
26939
  }
@@ -37659,14 +37717,14 @@ function serveStdio(factory, options = {}) {
37659
37717
  return false;
37660
37718
  };
37661
37719
  const answerLegacyRejection = (request, reason, requestedVersion) => {
37662
- const rejection2 = modernOnlyStrictRejection({
37720
+ const rejection3 = modernOnlyStrictRejection({
37663
37721
  kind: "legacy",
37664
37722
  reason,
37665
37723
  ...requestedVersion !== void 0 && { requestedVersion }
37666
37724
  }, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
37667
- if (rejection2 === void 0) return Promise.resolve();
37668
- reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection2.cell}): ${rejection2.message}`));
37669
- return writeErrorResponse(request.id, rejection2.code, rejection2.message, rejection2.data);
37725
+ if (rejection3 === void 0) return Promise.resolve();
37726
+ reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection3.cell}): ${rejection3.message}`));
37727
+ return writeErrorResponse(request.id, rejection3.code, rejection3.message, rejection3.data);
37670
37728
  };
37671
37729
  const onInstanceClosed = (channel) => {
37672
37730
  if (closing || channel === discarding) return;
@@ -58498,6 +58556,11 @@ var init_media = __esm({
58498
58556
  });
58499
58557
 
58500
58558
  // node_modules/@chrischall/mcp-utils/dist/response/index.js
58559
+ function textResult(data) {
58560
+ return {
58561
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
58562
+ };
58563
+ }
58501
58564
  function errorResult(message) {
58502
58565
  return {
58503
58566
  content: [{ type: "text", text: redactSecrets(message) }],
@@ -58512,12 +58575,446 @@ var init_response = __esm({
58512
58575
  }
58513
58576
  });
58514
58577
 
58578
+ // node_modules/@chrischall/mcp-utils/dist/server/canonical.js
58579
+ function canonicalJson(value) {
58580
+ if (value === null || typeof value === "string" || typeof value === "boolean")
58581
+ return JSON.stringify(value);
58582
+ if (value === void 0)
58583
+ return "null";
58584
+ if (typeof value === "number")
58585
+ return Number.isFinite(value) ? JSON.stringify(value) : `{"$num":"${String(value)}"}`;
58586
+ if (typeof value === "bigint")
58587
+ return `{"$bigint":"${value.toString()}"}`;
58588
+ if (typeof value === "function" || typeof value === "symbol") {
58589
+ throw new TypeError(`confirmation: cannot bind a ${typeof value} argument value.`);
58590
+ }
58591
+ if (Array.isArray(value))
58592
+ return `[${value.map(canonicalJson).join(",")}]`;
58593
+ if (value instanceof Date) {
58594
+ return `{"$date":${JSON.stringify(Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString())}}`;
58595
+ }
58596
+ if (ArrayBuffer.isView(value)) {
58597
+ const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
58598
+ return `{"$bytes":${JSON.stringify(bytes.toString("base64"))}}`;
58599
+ }
58600
+ if (value instanceof ArrayBuffer)
58601
+ return `{"$bytes":${JSON.stringify(Buffer.from(value).toString("base64"))}}`;
58602
+ if (value instanceof Map) {
58603
+ const entries2 = [...value.entries()].map(([k, v]) => `[${canonicalJson(k)},${canonicalJson(v)}]`).sort();
58604
+ return `{"$map":[${entries2.join(",")}]}`;
58605
+ }
58606
+ if (value instanceof Set) {
58607
+ return `{"$set":[${[...value].map(canonicalJson).sort().join(",")}]}`;
58608
+ }
58609
+ const proto = Object.getPrototypeOf(value);
58610
+ if (proto !== Object.prototype && proto !== null) {
58611
+ throw new TypeError("confirmation: cannot bind a non-plain object argument value (class instance).");
58612
+ }
58613
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
58614
+ return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
58615
+ }
58616
+ function escapeKey(key) {
58617
+ return key.startsWith("$") ? `$${key}` : key;
58618
+ }
58619
+ var init_canonical = __esm({
58620
+ "node_modules/@chrischall/mcp-utils/dist/server/canonical.js"() {
58621
+ }
58622
+ });
58623
+
58515
58624
  // node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
58625
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
58626
+ function bindingKey(binding) {
58627
+ if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
58628
+ throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
58629
+ }
58630
+ const key = typeof binding.key === "string" ? Buffer.from(binding.key, "utf8") : Buffer.from(binding.key);
58631
+ if (key.length < 32) {
58632
+ throw new RangeError("requireConfirmation: binding.key must be at least 32 bytes.");
58633
+ }
58634
+ return key;
58635
+ }
58636
+ function commitment(action, args) {
58637
+ return createHash("sha256").update(`${action}\0${canonicalJson(args)}`).digest("base64url");
58638
+ }
58639
+ function mintState(key, action, binding) {
58640
+ const exp = Math.floor(Date.now() / 1e3) + (binding.ttlSeconds ?? 600);
58641
+ const body = Buffer.from(JSON.stringify({ c: commitment(action, binding.args), exp })).toString("base64url");
58642
+ const mac3 = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest("base64url");
58643
+ return `${STATE_PREFIX}${body}.${mac3}`;
58644
+ }
58645
+ function verifyState(key, action, binding, state) {
58646
+ if (typeof state !== "string" || !state.startsWith(STATE_PREFIX))
58647
+ return false;
58648
+ const rest = state.slice(STATE_PREFIX.length);
58649
+ const dot = rest.indexOf(".");
58650
+ if (dot < 0)
58651
+ return false;
58652
+ const body = rest.slice(0, dot);
58653
+ const mac3 = Buffer.from(rest.slice(dot + 1), "base64url");
58654
+ const expected = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest();
58655
+ if (mac3.length !== expected.length || !timingSafeEqual(mac3, expected))
58656
+ return false;
58657
+ let payload;
58658
+ try {
58659
+ payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
58660
+ } catch {
58661
+ return false;
58662
+ }
58663
+ if (typeof payload.exp !== "number" || payload.exp * 1e3 <= Date.now())
58664
+ return false;
58665
+ const want = Buffer.from(commitment(action, binding.args));
58666
+ const got = Buffer.from(typeof payload.c === "string" ? payload.c : "");
58667
+ return got.length === want.length && timingSafeEqual(got, want);
58668
+ }
58669
+ function echoedState(ctx) {
58670
+ const accessor = ctx.mcpReq.requestState;
58671
+ return typeof accessor === "function" ? accessor() : void 0;
58672
+ }
58673
+ function requireConfirmation(ctx, options) {
58674
+ const requestKey = options.requestKey ?? DEFAULT_REQUEST_KEY;
58675
+ const key = options.binding ? bindingKey(options.binding) : void 0;
58676
+ const confirmationSchema = external_exports.object({
58677
+ confirmed: external_exports.boolean().describe(options.confirmationLabel ?? DEFAULT_CONFIRMATION_LABEL)
58678
+ });
58679
+ const ask = () => {
58680
+ const preview = {
58681
+ action: options.action,
58682
+ ...options.details === void 0 ? {} : { details: options.details }
58683
+ };
58684
+ return inputRequired({
58685
+ inputRequests: {
58686
+ [requestKey]: inputRequired.elicit({
58687
+ message: `${options.message}
58688
+ ${JSON.stringify(preview, null, 2)}`,
58689
+ requestedSchema: confirmationSchema
58690
+ })
58691
+ },
58692
+ ...key && options.binding ? { requestState: mintState(key, options.action, options.binding) } : {}
58693
+ });
58694
+ };
58695
+ const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
58696
+ const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
58697
+ if (response.kind === "missing") {
58698
+ if (callerAcceptsFormElicitation(ctx) === false) {
58699
+ return textResult({
58700
+ confirmed: false,
58701
+ dispatched: false,
58702
+ action: options.action,
58703
+ reason: "confirmation-unsupported",
58704
+ note: options.unsupportedNote ? `${UNSUPPORTED_NOTE}. ${options.unsupportedNote}` : `${UNSUPPORTED_NOTE}.`
58705
+ });
58706
+ }
58707
+ return ask();
58708
+ }
58709
+ if (response.kind === "elicit" && response.action === "accept" && accepted?.confirmed === true) {
58710
+ if (key && options.binding) {
58711
+ const state = echoedState(ctx);
58712
+ if (state === void 0 || state === null) {
58713
+ return errorResult(`Confirmation for ${options.action} was accepted, but the retry carried no requestState, so it cannot be checked against the prompt that was shown. Nothing was done. The likely cause is that the MCP client or host does not round-trip requestState (it must echo the requestState from the input_required result back on the retry); asking again would loop.`);
58714
+ }
58715
+ if (!verifyState(key, options.action, options.binding, state))
58716
+ return ask();
58717
+ }
58718
+ return void 0;
58719
+ }
58720
+ return textResult({
58721
+ confirmed: false,
58722
+ cancelled: true,
58723
+ action: options.action,
58724
+ note: "Nothing was changed because the confirmation was declined, cancelled, or left unchecked."
58725
+ });
58726
+ }
58727
+ var DEFAULT_REQUEST_KEY, DEFAULT_CONFIRMATION_LABEL, UNSUPPORTED_NOTE, STATE_PREFIX;
58516
58728
  var init_confirmation = __esm({
58517
58729
  "node_modules/@chrischall/mcp-utils/dist/server/confirmation.js"() {
58518
58730
  init_dist();
58731
+ init_zod();
58519
58732
  init_caller();
58733
+ init_canonical();
58520
58734
  init_response();
58735
+ DEFAULT_REQUEST_KEY = "confirmation";
58736
+ DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
58737
+ 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";
58738
+ STATE_PREFIX = "mcpu.confirm.v1.";
58739
+ }
58740
+ });
58741
+
58742
+ // node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js
58743
+ import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
58744
+ function createSpentTokenStore() {
58745
+ const spent = /* @__PURE__ */ new Map();
58746
+ return {
58747
+ has: (nonce) => spent.has(nonce),
58748
+ add: (nonce, exp) => {
58749
+ spent.set(nonce, exp);
58750
+ },
58751
+ prune: (now) => {
58752
+ for (const [nonce, exp] of spent)
58753
+ if (exp < now)
58754
+ spent.delete(nonce);
58755
+ },
58756
+ clear: () => spent.clear(),
58757
+ get size() {
58758
+ return spent.size;
58759
+ }
58760
+ };
58761
+ }
58762
+ function tokenKey(key) {
58763
+ const bytes = typeof key === "string" ? Buffer.from(key, "utf8") : Buffer.from(key);
58764
+ if (bytes.length < 32)
58765
+ throw new RangeError("confirm token: key must be at least 32 bytes.");
58766
+ return bytes;
58767
+ }
58768
+ function ttlOf(ttlSeconds) {
58769
+ if (ttlSeconds === void 0)
58770
+ return DEFAULT_TTL_SECONDS;
58771
+ if (!(Number.isFinite(ttlSeconds) && ttlSeconds > 0)) {
58772
+ throw new RangeError("confirm token: ttlSeconds must be a finite number greater than 0.");
58773
+ }
58774
+ return ttlSeconds;
58775
+ }
58776
+ function sign(key, body) {
58777
+ return createHmac2("sha256", key).update(`${PREFIX}${body}`).digest();
58778
+ }
58779
+ function hashConfirmPayload(payload) {
58780
+ return createHash2("sha256").update(canonicalJson(payload)).digest("base64url");
58781
+ }
58782
+ function issueConfirmToken(key, binding, options = {}) {
58783
+ const k = tokenKey(key);
58784
+ const now = options.now ?? Date.now();
58785
+ const exp = now + ttlOf(options.ttlSeconds) * 1e3;
58786
+ const claims = {
58787
+ t: binding.tool,
58788
+ ...binding.account === void 0 ? {} : { a: binding.account },
58789
+ g: binding.target,
58790
+ ...binding.revision === void 0 ? {} : { r: binding.revision },
58791
+ h: binding.payloadHash,
58792
+ exp,
58793
+ n: randomBytes(16).toString("base64url")
58794
+ };
58795
+ const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
58796
+ return { token: `${PREFIX}${body}.${sign(k, body).toString("base64url")}`, expiresAt: new Date(exp).toISOString() };
58797
+ }
58798
+ function parseClaims(body) {
58799
+ try {
58800
+ const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
58801
+ return claims && typeof claims === "object" ? claims : void 0;
58802
+ } catch {
58803
+ return void 0;
58804
+ }
58805
+ }
58806
+ function verifyConfirmToken(key, token, binding, options = {}) {
58807
+ const k = tokenKey(key);
58808
+ const now = options.now ?? Date.now();
58809
+ const spent = options.spent ?? PROCESS_SPENT;
58810
+ spent.prune(now);
58811
+ if (!token.startsWith(PREFIX))
58812
+ return { ok: false, error: "TOKEN_INVALID" };
58813
+ const rest = token.slice(PREFIX.length);
58814
+ const dot = rest.indexOf(".");
58815
+ const body = dot < 0 ? "" : rest.slice(0, dot);
58816
+ const mac3 = Buffer.from(dot < 0 ? "" : rest.slice(dot + 1), "base64url");
58817
+ const expected = sign(k, body);
58818
+ if (!body || mac3.length !== expected.length || !timingSafeEqual2(mac3, expected))
58819
+ return { ok: false, error: "TOKEN_INVALID" };
58820
+ const claims = parseClaims(body);
58821
+ if (!claims)
58822
+ return { ok: false, error: "TOKEN_INVALID" };
58823
+ if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
58824
+ return { ok: false, error: "TOKEN_INVALID" };
58825
+ }
58826
+ if (spent.has(claims.n))
58827
+ return { ok: false, error: "TOKEN_REUSED" };
58828
+ if (now > claims.exp)
58829
+ return { ok: false, error: "TOKEN_EXPIRED" };
58830
+ if (claims.r !== binding.revision)
58831
+ return { ok: false, error: "DRAFT_CHANGED", reason: "revision-changed" };
58832
+ if (claims.h !== binding.payloadHash)
58833
+ return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
58834
+ spent.add(claims.n, claims.exp);
58835
+ return { ok: true };
58836
+ }
58837
+ function isToolResult(value) {
58838
+ return Array.isArray(value.content);
58839
+ }
58840
+ function rejection2(data) {
58841
+ return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
58842
+ }
58843
+ async function tokenConfirmation(action, fb) {
58844
+ const subject = await fb.subject();
58845
+ if (isToolResult(subject))
58846
+ return subject;
58847
+ const binding = {
58848
+ tool: fb.tool,
58849
+ ...fb.account === void 0 ? {} : { account: fb.account },
58850
+ target: subject.target,
58851
+ ...subject.revision === void 0 ? {} : { revision: subject.revision },
58852
+ payloadHash: hashConfirmPayload(subject.payload)
58853
+ };
58854
+ const phaseOne = () => {
58855
+ const { token, expiresAt } = issueConfirmToken(fb.key, binding, { ttlSeconds: fb.ttlSeconds });
58856
+ return {
58857
+ action,
58858
+ preview: subject.preview,
58859
+ confirmToken: token,
58860
+ expiresAt,
58861
+ ttlSeconds: ttlOf(fb.ttlSeconds),
58862
+ instruction: fb.instruction ?? CONFIRM_TOKEN_INSTRUCTION
58863
+ };
58864
+ };
58865
+ if (!fb.confirmToken) {
58866
+ return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
58867
+ }
58868
+ const verdict = verifyConfirmToken(fb.key, fb.confirmToken, binding, { spent: fb.spent });
58869
+ if (verdict.ok)
58870
+ return void 0;
58871
+ if (verdict.error === "DRAFT_CHANGED") {
58872
+ return rejection2({
58873
+ error: "DRAFT_CHANGED",
58874
+ reason: verdict.reason,
58875
+ note: `${CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
58876
+ ...phaseOne()
58877
+ });
58878
+ }
58879
+ return rejection2({ error: verdict.error, action, note: TOKEN_ERROR_NOTE[verdict.error] });
58880
+ }
58881
+ async function requireConfirmationWithFallback(ctx, options) {
58882
+ const { tokenFallback, ...confirmation } = options;
58883
+ if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
58884
+ return tokenConfirmation(options.action, tokenFallback);
58885
+ }
58886
+ return requireConfirmation(ctx, confirmation);
58887
+ }
58888
+ var PROCESS_SPENT, PREFIX, DEFAULT_TTL_SECONDS, CONFIRM_TOKEN_INSTRUCTION, confirmTokenParam, TOKEN_ERROR_NOTE, CHANGED_NOTE;
58889
+ var init_confirm_token = __esm({
58890
+ "node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js"() {
58891
+ init_zod();
58892
+ init_caller();
58893
+ init_response();
58894
+ init_canonical();
58895
+ init_confirmation();
58896
+ PROCESS_SPENT = createSpentTokenStore();
58897
+ PREFIX = "mcpu.token.v1.";
58898
+ DEFAULT_TTL_SECONDS = 600;
58899
+ CONFIRM_TOKEN_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
58900
+ confirmTokenParam = external_exports.string().optional().describe(`ONLY for the two-step confirmation fallback (a client without MCP elicitation). The confirmToken from this same tool's phase-1 "confirmation-required" response, passed back ONLY after the user has seen that preview and explicitly approved it in chat \u2014 never on the first call, never invented, never reused. Call again with the same arguments. Ignored when the client supports elicitation.`);
58901
+ TOKEN_ERROR_NOTE = {
58902
+ TOKEN_EXPIRED: "Nothing was sent or changed: the confirmToken expired. Call again WITHOUT confirmToken for a fresh preview, and ask the user to approve it again.",
58903
+ TOKEN_REUSED: "Nothing was sent or changed by this call: this confirmToken was already used, and one approval acts once. If doing it again is really intended, call again WITHOUT confirmToken and get a new approval.",
58904
+ TOKEN_INVALID: "Nothing was sent or changed: this confirmToken was not issued by this server for this tool, account and target (or the server has restarted since). Call again WITHOUT confirmToken for a fresh preview and approval."
58905
+ };
58906
+ CHANGED_NOTE = {
58907
+ "revision-changed": "Nothing was sent or changed: the target was edited since the user approved it (its version rotated), so what would happen is not what they saw.",
58908
+ "payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
58909
+ };
58910
+ }
58911
+ });
58912
+
58913
+ // node_modules/@chrischall/mcp-utils/dist/config/index.js
58914
+ import { homedir } from "node:os";
58915
+ import { isAbsolute, join, resolve } from "node:path";
58916
+ function readEnvVar(key, opts = {}) {
58917
+ const env = opts.env ?? process.env;
58918
+ const raw = env[key];
58919
+ if (typeof raw === "string") {
58920
+ const trimmed = raw.trim();
58921
+ if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
58922
+ return trimmed;
58923
+ }
58924
+ }
58925
+ return opts.default;
58926
+ }
58927
+ function expandPath(p) {
58928
+ let expanded = p;
58929
+ if (p === "~") {
58930
+ expanded = homedir();
58931
+ } else if (p.startsWith("~/")) {
58932
+ expanded = join(homedir(), p.slice(2));
58933
+ }
58934
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
58935
+ }
58936
+ async function loadDotenvSafely(opts = {}) {
58937
+ try {
58938
+ const mod = await import(
58939
+ /* @vite-ignore */
58940
+ "dotenv"
58941
+ );
58942
+ const result = mod.config({
58943
+ ...opts.path !== void 0 ? { path: opts.path } : {},
58944
+ override: opts.override ?? false,
58945
+ quiet: true
58946
+ });
58947
+ return result.error === void 0;
58948
+ } catch {
58949
+ return false;
58950
+ }
58951
+ }
58952
+ var PLACEHOLDER_RE;
58953
+ var init_config = __esm({
58954
+ "node_modules/@chrischall/mcp-utils/dist/config/index.js"() {
58955
+ PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
58956
+ }
58957
+ });
58958
+
58959
+ // node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
58960
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
58961
+ function readConfirmMode(env = process.env) {
58962
+ const raw = readEnvVar("MCP_CONFIRM_MODE", { env })?.trim().toLowerCase();
58963
+ if (!raw)
58964
+ return "ask-user";
58965
+ if (MODES.has(raw))
58966
+ return raw;
58967
+ if (!warned.has(raw)) {
58968
+ warned.add(raw);
58969
+ process.stderr.write(`MCP_CONFIRM_MODE="${raw}" is not one of ask-user, auto, refuse; treating it as refuse.
58970
+ `);
58971
+ }
58972
+ return "refuse";
58973
+ }
58974
+ function confirmTtlFromEnv(env = process.env) {
58975
+ const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
58976
+ return raw && /^[1-9]\d*$/.test(raw) ? Number(raw) : DEFAULT_TTL_SECONDS2;
58977
+ }
58978
+ function confirmKeyFromEnv(env = process.env) {
58979
+ const secret = readEnvVar("MCP_CONFIRM_SECRET", { env });
58980
+ if (secret)
58981
+ return createHash3("sha256").update(secret, "utf8").digest();
58982
+ processKey ??= randomBytes2(32);
58983
+ return processKey;
58984
+ }
58985
+ function confirmationFromEnv(options) {
58986
+ const { tool, account, confirmToken, subject, instruction, spent, env = process.env, ...confirmation } = options;
58987
+ const mode = readConfirmMode(env);
58988
+ if (mode === "refuse") {
58989
+ return {
58990
+ ...confirmation,
58991
+ unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${REFUSE_HINT}` : REFUSE_HINT
58992
+ };
58993
+ }
58994
+ return {
58995
+ ...confirmation,
58996
+ tokenFallback: {
58997
+ key: confirmKeyFromEnv(env),
58998
+ tool,
58999
+ ...account === void 0 ? {} : { account },
59000
+ ...confirmToken === void 0 ? {} : { confirmToken },
59001
+ subject,
59002
+ ttlSeconds: confirmTtlFromEnv(env),
59003
+ instruction: mode === "auto" ? CONFIRM_TOKEN_AUTO_INSTRUCTION : instruction ?? CONFIRM_TOKEN_INSTRUCTION,
59004
+ ...spent === void 0 ? {} : { spent }
59005
+ }
59006
+ };
59007
+ }
59008
+ var MODES, DEFAULT_TTL_SECONDS2, warned, processKey, CONFIRM_TOKEN_AUTO_INSTRUCTION, REFUSE_HINT;
59009
+ var init_confirm_env = __esm({
59010
+ "node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js"() {
59011
+ init_config();
59012
+ init_confirm_token();
59013
+ MODES = /* @__PURE__ */ new Set(["ask-user", "auto", "refuse"]);
59014
+ DEFAULT_TTL_SECONDS2 = 600;
59015
+ warned = /* @__PURE__ */ new Set();
59016
+ 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.)";
59017
+ REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
58521
59018
  }
58522
59019
  });
58523
59020
 
@@ -58697,6 +59194,8 @@ var init_server = __esm({
58697
59194
  init_errors();
58698
59195
  init_response();
58699
59196
  init_confirmation();
59197
+ init_confirm_token();
59198
+ init_confirm_env();
58700
59199
  init_cancel();
58701
59200
  init_caller();
58702
59201
  SERVER_PROTOCOL_VERSIONS = Object.freeze([
@@ -58708,52 +59207,6 @@ var init_server = __esm({
58708
59207
  }
58709
59208
  });
58710
59209
 
58711
- // node_modules/@chrischall/mcp-utils/dist/config/index.js
58712
- import { homedir } from "node:os";
58713
- import { isAbsolute, join, resolve } from "node:path";
58714
- function readEnvVar(key, opts = {}) {
58715
- const env = opts.env ?? process.env;
58716
- const raw = env[key];
58717
- if (typeof raw === "string") {
58718
- const trimmed = raw.trim();
58719
- if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
58720
- return trimmed;
58721
- }
58722
- }
58723
- return opts.default;
58724
- }
58725
- function expandPath(p) {
58726
- let expanded = p;
58727
- if (p === "~") {
58728
- expanded = homedir();
58729
- } else if (p.startsWith("~/")) {
58730
- expanded = join(homedir(), p.slice(2));
58731
- }
58732
- return isAbsolute(expanded) ? expanded : resolve(expanded);
58733
- }
58734
- async function loadDotenvSafely(opts = {}) {
58735
- try {
58736
- const mod = await import(
58737
- /* @vite-ignore */
58738
- "dotenv"
58739
- );
58740
- const result = mod.config({
58741
- ...opts.path !== void 0 ? { path: opts.path } : {},
58742
- override: opts.override ?? false,
58743
- quiet: true
58744
- });
58745
- return result.error === void 0;
58746
- } catch {
58747
- return false;
58748
- }
58749
- }
58750
- var PLACEHOLDER_RE;
58751
- var init_config = __esm({
58752
- "node_modules/@chrischall/mcp-utils/dist/config/index.js"() {
58753
- PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
58754
- }
58755
- });
58756
-
58757
59210
  // node_modules/@chrischall/mcp-utils/dist/fs/confine.js
58758
59211
  import { existsSync, realpathSync } from "node:fs";
58759
59212
  import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
@@ -59513,7 +59966,7 @@ async function loadStdioDotenv() {
59513
59966
  }
59514
59967
 
59515
59968
  // src/version.ts
59516
- var VERSION = "2.1.4";
59969
+ var VERSION = "2.2.0";
59517
59970
 
59518
59971
  // src/errors.ts
59519
59972
  init_dist2();
@@ -60942,21 +61395,21 @@ function assertLocalInputsAvailable(sink, args) {
60942
61395
 
60943
61396
  // src/jobs.ts
60944
61397
  init_dist2();
60945
- import { randomUUID, createHash as createHash2 } from "node:crypto";
61398
+ import { randomUUID, createHash as createHash5 } from "node:crypto";
60946
61399
 
60947
61400
  // src/job-store.ts
60948
- import { createHash } from "node:crypto";
61401
+ import { createHash as createHash4 } from "node:crypto";
60949
61402
  var HEARTBEAT_MS = 6e4;
60950
61403
  var EXECUTOR_LOST_AFTER_MS = 3 * HEARTBEAT_MS;
60951
- var PREFIX = "jobs";
61404
+ var PREFIX2 = "jobs";
60952
61405
  function keyHash(idempotencyKey) {
60953
- return createHash("sha256").update(idempotencyKey).digest("hex").slice(0, 32);
61406
+ return createHash4("sha256").update(idempotencyKey).digest("hex").slice(0, 32);
60954
61407
  }
60955
61408
  function createBlobJobStore(bucket, opts) {
60956
61409
  const now = opts.now ?? (() => Date.now());
60957
61410
  const tenantOf = async () => typeof opts.tenant === "function" ? opts.tenant() : opts.tenant;
60958
- const recordKey = async (jobId) => `${PREFIX}/${await tenantOf()}/${encodeURIComponent(jobId)}.json`;
60959
- const pointerKey = async (k) => `${PREFIX}/${await tenantOf()}/keys/${keyHash(k)}.json`;
61411
+ const recordKey = async (jobId) => `${PREFIX2}/${await tenantOf()}/${encodeURIComponent(jobId)}.json`;
61412
+ const pointerKey = async (k) => `${PREFIX2}/${await tenantOf()}/keys/${keyHash(k)}.json`;
60960
61413
  async function writeJson(key, value) {
60961
61414
  try {
60962
61415
  await bucket.put(key, JSON.stringify(value), { httpMetadata: { contentType: "application/json" } });
@@ -61100,7 +61553,7 @@ function isExpired(entry, now) {
61100
61553
  return entry.settledAt !== void 0 && now - entry.settledAt > JOB_TTL_MS;
61101
61554
  }
61102
61555
  function fingerprintRequest(toolName, parts) {
61103
- return createHash2("sha256").update(`${toolName}\0${JSON.stringify(parts)}`).digest("hex");
61556
+ return createHash5("sha256").update(`${toolName}\0${JSON.stringify(parts)}`).digest("hex");
61104
61557
  }
61105
61558
  function replayKey(idempotencyKey, fingerprint) {
61106
61559
  return `${idempotencyKey}\0${fingerprint}`;
@@ -61693,24 +62146,43 @@ function attachCost(meta3, model, usage) {
61693
62146
  // src/tools/_confirm.ts
61694
62147
  init_dist2();
61695
62148
  init_images();
61696
- function previewUnlessConfirmed(confirm, action, method, path, body) {
61697
- if (confirm === true) return null;
61698
- return minifiedResult({
61699
- dryRun: true,
61700
- action,
61701
- method,
61702
- path,
61703
- ...body !== void 0 ? { willSend: body } : {},
61704
- note: "Re-run with confirm: true to execute."
61705
- });
62149
+ function confirmNote(lead = "Asks the user to confirm first") {
62150
+ return `${lead}: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).`;
62151
+ }
62152
+ async function confirmWrite(ctx, o) {
62153
+ const preview = {
62154
+ action: o.description,
62155
+ method: o.method,
62156
+ path: o.path,
62157
+ ...o.body !== void 0 ? { willSend: o.body } : {}
62158
+ };
62159
+ return requireConfirmationWithFallback(ctx, confirmationFromEnv({
62160
+ action: o.action,
62161
+ message: o.message,
62162
+ details: preview,
62163
+ tool: o.tool,
62164
+ confirmToken: o.confirmToken,
62165
+ subject: () => ({ target: o.target, payload: o.payload, preview })
62166
+ }));
61706
62167
  }
61707
- async function previewLocalInputsUnlessConfirmed(confirm, action, endpoint, imagePaths, videoPath) {
61708
- const hasLocal = (imagePaths?.length ?? 0) > 0 || Boolean(videoPath);
61709
- if (confirm === true || !hasLocal) return null;
62168
+ async function confirmLocalInputs(ctx, o) {
62169
+ const hasLocal = (o.imagePaths?.length ?? 0) > 0 || Boolean(o.videoPath);
62170
+ if (!hasLocal) return void 0;
61710
62171
  const inputs = [];
61711
- for (const p of imagePaths ?? []) inputs.push(await previewImageInput(p));
61712
- if (videoPath) inputs.push(await previewVideoInput(videoPath));
61713
- return previewUnlessConfirmed(confirm, action, "POST", endpoint, { inputs });
62172
+ for (const p of o.imagePaths ?? []) inputs.push(await previewImageInput(p));
62173
+ if (o.videoPath) inputs.push(await previewVideoInput(o.videoPath));
62174
+ return confirmWrite(ctx, {
62175
+ tool: o.tool,
62176
+ action: o.action,
62177
+ message: "Review and confirm sending these local files to Google:",
62178
+ description: o.description,
62179
+ method: "POST",
62180
+ path: o.endpoint,
62181
+ body: { inputs },
62182
+ target: "",
62183
+ payload: { request: o.request, inputs },
62184
+ confirmToken: o.confirmToken
62185
+ });
61714
62186
  }
61715
62187
 
61716
62188
  // src/tools/generate.ts
@@ -61721,7 +62193,7 @@ function registerGenerateTools(server, client2) {
61721
62193
  server.registerTool(
61722
62194
  "gemini_image_generate",
61723
62195
  {
61724
- 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.",
62196
+ 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"),
61725
62197
  annotations: { readOnlyHint: false, openWorldHint: true },
61726
62198
  inputSchema: external_exports.object({
61727
62199
  prompt: external_exports.string().min(1).describe("Text prompt describing the image"),
@@ -61736,13 +62208,23 @@ function registerGenerateTools(server, client2) {
61736
62208
  style: styleSchema,
61737
62209
  video_url: external_exports.string().url().optional().describe("Public YouTube URL (or a previously uploaded Files API uri) as a video reference (video\u2192image; use a Flash model e.g. gemini-3.1-flash-image)"),
61738
62210
  video_path: videoPathSchema,
61739
- confirm: schemaConfirm,
62211
+ confirmToken: confirmTokenParam,
61740
62212
  ...sharedImageSchema
61741
62213
  })
61742
62214
  },
61743
62215
  async (args, extra) => {
61744
62216
  assertLocalInputsAvailable(client2.mediaSink, args);
61745
- const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images, args.video_path);
62217
+ const { confirmToken, ...request } = args;
62218
+ const gate = await confirmLocalInputs(extra, {
62219
+ tool: "gemini_image_generate",
62220
+ action: "image.generate",
62221
+ description: SEND_LOCAL_ACTION,
62222
+ endpoint: GENERATE_ENDPOINT,
62223
+ imagePaths: args.images,
62224
+ videoPath: args.video_path,
62225
+ request,
62226
+ confirmToken
62227
+ });
61746
62228
  if (gate) return gate;
61747
62229
  const count = args.count ?? 1;
61748
62230
  const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
@@ -61831,7 +62313,7 @@ function registerGenerateTools(server, client2) {
61831
62313
  server.registerTool(
61832
62314
  "gemini_image_edit",
61833
62315
  {
61834
- 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.",
62316
+ 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"),
61835
62317
  annotations: { readOnlyHint: false, openWorldHint: true },
61836
62318
  inputSchema: external_exports.object({
61837
62319
  prompt: external_exports.string().min(1).describe("Instruction describing the edit or composition"),
@@ -61843,14 +62325,23 @@ function registerGenerateTools(server, client2) {
61843
62325
  characters: charactersSchema,
61844
62326
  style: styleSchema,
61845
62327
  filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
61846
- confirm: schemaConfirm,
62328
+ confirmToken: confirmTokenParam,
61847
62329
  ...sharedImageSchema
61848
62330
  })
61849
62331
  },
61850
62332
  async (args, extra) => {
61851
62333
  assertLocalInputsAvailable(client2.mediaSink, args);
61852
62334
  if (!args.characters?.length) requireImageInput(args);
61853
- const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images);
62335
+ const { confirmToken, ...request } = args;
62336
+ const gate = await confirmLocalInputs(extra, {
62337
+ tool: "gemini_image_edit",
62338
+ action: "image.edit",
62339
+ description: SEND_LOCAL_ACTION,
62340
+ endpoint: GENERATE_ENDPOINT,
62341
+ imagePaths: args.images,
62342
+ request,
62343
+ confirmToken
62344
+ });
61854
62345
  if (gate) return gate;
61855
62346
  const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
61856
62347
  const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
@@ -61913,7 +62404,7 @@ function registerSetTools(server, client2) {
61913
62404
  server.registerTool(
61914
62405
  "gemini_image_set",
61915
62406
  {
61916
- 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.',
62407
+ 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"),
61917
62408
  annotations: { readOnlyHint: false, openWorldHint: true },
61918
62409
  inputSchema: external_exports.object({
61919
62410
  master_prompt: external_exports.string().min(1).describe("Prompt for the master/reference image"),
@@ -61928,7 +62419,7 @@ function registerSetTools(server, client2) {
61928
62419
  characters: charactersSchema,
61929
62420
  style: styleSchema,
61930
62421
  master_images_base64: imagesBase64Schema("Reference images for the master generation", "master_images"),
61931
- confirm: schemaConfirm,
62422
+ confirmToken: confirmTokenParam,
61932
62423
  ...sharedImageSchema
61933
62424
  })
61934
62425
  },
@@ -61937,7 +62428,16 @@ function registerSetTools(server, client2) {
61937
62428
  if (args.scenes && args.count || !args.scenes && !args.count) {
61938
62429
  throw new McpToolError("Provide exactly one of `scenes` or `count`.");
61939
62430
  }
61940
- const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local master image input(s) to the Gemini API", "/v1beta/models/{model}:generateContent", args.master_images);
62431
+ const { confirmToken, ...request } = args;
62432
+ const gate = await confirmLocalInputs(extra, {
62433
+ tool: "gemini_image_set",
62434
+ action: "image.set",
62435
+ description: "Send local master image input(s) to the Gemini API",
62436
+ endpoint: "/v1beta/models/{model}:generateContent",
62437
+ imagePaths: args.master_images,
62438
+ request,
62439
+ confirmToken
62440
+ });
61941
62441
  if (gate) return gate;
61942
62442
  const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
61943
62443
  const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
@@ -63429,7 +63929,7 @@ function registerInteractTools(server, client2) {
63429
63929
  server.registerTool(
63430
63930
  "gemini_interact",
63431
63931
  {
63432
- 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.",
63932
+ 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"),
63433
63933
  annotations: { readOnlyHint: false, openWorldHint: true },
63434
63934
  inputSchema: external_exports.object({
63435
63935
  input: external_exports.string().min(1).describe("Text prompt or editing instruction"),
@@ -63458,7 +63958,7 @@ function registerInteractTools(server, client2) {
63458
63958
  async: asyncSchema,
63459
63959
  max_wait_ms: maxWaitMsSchema,
63460
63960
  from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)"),
63461
- confirm: schemaConfirm
63961
+ confirmToken: confirmTokenParam
63462
63962
  })
63463
63963
  },
63464
63964
  async (args, extra) => {
@@ -63483,7 +63983,17 @@ function registerInteractTools(server, client2) {
63483
63983
  if (previousInteractionId && images?.length) {
63484
63984
  ({ kept: images, dropped } = splitReattachedOutputs(images, client2.session.writtenOutputs));
63485
63985
  }
63486
- const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local file input(s) to the Gemini Interactions API", "/v1beta/interactions", images, args.video_path);
63986
+ const { confirmToken, ...request } = args;
63987
+ const gate = await confirmLocalInputs(extra, {
63988
+ tool: "gemini_interact",
63989
+ action: "interaction.create",
63990
+ description: "Send local file input(s) to the Gemini Interactions API",
63991
+ endpoint: "/v1beta/interactions",
63992
+ imagePaths: images,
63993
+ videoPath: args.video_path,
63994
+ request: { ...request, images, previous_interaction_id: previousInteractionId },
63995
+ confirmToken
63996
+ });
63487
63997
  if (gate) return gate;
63488
63998
  const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
63489
63999
  const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
@@ -63592,7 +64102,7 @@ function registerVideoTools(server, client2) {
63592
64102
  server.registerTool(
63593
64103
  "gemini_video_generate",
63594
64104
  {
63595
- 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.',
64105
+ 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"),
63596
64106
  annotations: { readOnlyHint: false, openWorldHint: true },
63597
64107
  inputSchema: external_exports.object({
63598
64108
  prompt: external_exports.string().min(1).describe("Description of the video to generate (or the edit instruction when task=edit)"),
@@ -63616,7 +64126,7 @@ function registerVideoTools(server, client2) {
63616
64126
  idempotency_key: idempotencyKeySchema,
63617
64127
  async: asyncSchema,
63618
64128
  max_wait_ms: maxWaitMsSchema,
63619
- confirm: schemaConfirm
64129
+ confirmToken: confirmTokenParam
63620
64130
  })
63621
64131
  },
63622
64132
  async (args, extra) => {
@@ -63630,7 +64140,16 @@ function registerVideoTools(server, client2) {
63630
64140
  }
63631
64141
  previousInteractionId = client2.session.lastVideoInteractionId;
63632
64142
  }
63633
- const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local image input(s) to the Gemini omni API", "/v1beta/interactions", args.images);
64143
+ const { confirmToken, ...request } = args;
64144
+ const gate = await confirmLocalInputs(extra, {
64145
+ tool: "gemini_video_generate",
64146
+ action: "video.generate",
64147
+ description: "Send local image input(s) to the Gemini omni API",
64148
+ endpoint: "/v1beta/interactions",
64149
+ imagePaths: args.images,
64150
+ request: { ...request, previous_interaction_id: previousInteractionId },
64151
+ confirmToken
64152
+ });
63634
64153
  if (gate) return gate;
63635
64154
  const model = args.model?.trim() || DEFAULT_VIDEO_MODEL;
63636
64155
  const aspectRatio = resolveAspectRatio(args, VIDEO_ASPECT_RATIOS);
@@ -63695,7 +64214,7 @@ function registerMusicTools(server, client2) {
63695
64214
  server.registerTool(
63696
64215
  "gemini_music_generate",
63697
64216
  {
63698
- 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.",
64217
+ 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"),
63699
64218
  annotations: { readOnlyHint: false, openWorldHint: true },
63700
64219
  inputSchema: external_exports.object({
63701
64220
  prompt: external_exports.string().min(1).describe("Description of the music: mood, genre, instruments, tempo, structure, or lyrics"),
@@ -63713,13 +64232,22 @@ function registerMusicTools(server, client2) {
63713
64232
  idempotency_key: idempotencyKeySchema,
63714
64233
  async: asyncSchema,
63715
64234
  max_wait_ms: maxWaitMsSchema,
63716
- confirm: schemaConfirm
64235
+ confirmToken: confirmTokenParam
63717
64236
  })
63718
64237
  },
63719
64238
  async (args, extra) => {
63720
64239
  assertLocalInputsAvailable(client2.mediaSink, args);
63721
64240
  const model = args.model ?? DEFAULT_MUSIC_MODEL;
63722
- const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local image input(s) to the Gemini Lyria API", "/v1beta/interactions", args.images);
64241
+ const { confirmToken, ...request } = args;
64242
+ const gate = await confirmLocalInputs(extra, {
64243
+ tool: "gemini_music_generate",
64244
+ action: "music.generate",
64245
+ description: "Send local image input(s) to the Gemini Lyria API",
64246
+ endpoint: "/v1beta/interactions",
64247
+ imagePaths: args.images,
64248
+ request,
64249
+ confirmToken
64250
+ });
63723
64251
  if (gate) return gate;
63724
64252
  const fingerprint = fingerprintRequest("gemini_music_generate", {
63725
64253
  model,
@@ -63907,7 +64435,7 @@ function registerFileTools(server, client2) {
63907
64435
  server.registerTool(
63908
64436
  "gemini_upload_file",
63909
64437
  {
63910
- 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 ? "" : " 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."),
64438
+ 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."),
63911
64439
  annotations: { readOnlyHint: false, openWorldHint: true },
63912
64440
  inputSchema: external_exports.object({
63913
64441
  url: external_exports.string().url().optional().describe(
@@ -63922,7 +64450,7 @@ function registerFileTools(server, client2) {
63922
64450
  ),
63923
64451
  mime_type: external_exports.string().regex(/^[\w.+-]+\/[\w.+-]+$/, "must be a MIME type like image/png").optional().describe("Override the detected MIME type (sniffed from the bytes / taken from the server response otherwise)"),
63924
64452
  display_name: external_exports.string().min(1).optional().describe("Human-readable name recorded against the upload"),
63925
- confirm: schemaConfirm
64453
+ confirmToken: confirmTokenParam
63926
64454
  })
63927
64455
  },
63928
64456
  async (args, extra) => {
@@ -63949,12 +64477,21 @@ function registerFileTools(server, client2) {
63949
64477
  { hint: "Pass mime_type explicitly." }
63950
64478
  );
63951
64479
  }
63952
- if (args.confirm !== true) {
63953
- const { size } = await stat3(resolved);
63954
- return previewUnlessConfirmed(args.confirm, "Upload a local file to the Gemini Files API", "POST", "/upload/v1beta/files", {
63955
- inputs: [{ path: resolved, mimeType, size }]
63956
- });
63957
- }
64480
+ const { size } = await stat3(resolved);
64481
+ const inputs = [{ path: resolved, mimeType, size }];
64482
+ const gate = await confirmWrite(extra, {
64483
+ tool: "gemini_upload_file",
64484
+ action: "file.upload",
64485
+ message: "Review and confirm uploading this local file to Google:",
64486
+ description: "Upload a local file to the Gemini Files API",
64487
+ method: "POST",
64488
+ path: "/upload/v1beta/files",
64489
+ body: { inputs },
64490
+ target: resolved,
64491
+ payload: { inputs, display_name: args.display_name },
64492
+ confirmToken: args.confirmToken
64493
+ });
64494
+ if (gate) return gate;
63958
64495
  localUpload = { path: resolved, mimeType };
63959
64496
  }
63960
64497
  const uploaded = await withProgressHeartbeat(extra, "Uploading to the Gemini Files API", async () => {
@@ -64125,15 +64662,25 @@ function registerFileTools(server, client2) {
64125
64662
  server.registerTool(
64126
64663
  "gemini_delete_file",
64127
64664
  {
64128
- description: "Delete an uploaded file, image or photo (by file_uri) from the Gemini Files API before its ~48h expiry. Any tool call still referencing it will then fail with a generic 404, so delete only references you are finished with.",
64665
+ description: "Delete an uploaded file, image or photo (by file_uri) from the Gemini Files API before its ~48h expiry. Any tool call still referencing it will then fail with a generic 404, so delete only references you are finished with. " + confirmNote(),
64129
64666
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
64130
64667
  inputSchema: external_exports.object({
64131
64668
  file_uri: external_exports.string().min(1).describe("The `files/<id>` reference (or full uri) to delete"),
64132
- confirm: schemaConfirm
64669
+ confirmToken: confirmTokenParam
64133
64670
  })
64134
64671
  },
64135
- async (args) => {
64136
- const gate = previewUnlessConfirmed(args.confirm, "Delete an uploaded Gemini file", "DELETE", `/v1beta/${args.file_uri}`);
64672
+ async (args, extra) => {
64673
+ const gate = await confirmWrite(extra, {
64674
+ tool: "gemini_delete_file",
64675
+ action: "file.delete",
64676
+ message: "Review and confirm this deletion:",
64677
+ description: "Delete an uploaded Gemini file",
64678
+ method: "DELETE",
64679
+ path: `/v1beta/${args.file_uri}`,
64680
+ target: args.file_uri,
64681
+ payload: { file_uri: args.file_uri },
64682
+ confirmToken: args.confirmToken
64683
+ });
64137
64684
  if (gate) return gate;
64138
64685
  await client2.deleteFile(args.file_uri);
64139
64686
  return minifiedResult({ deleted: args.file_uri });
@@ -64269,12 +64816,22 @@ function registerLibraryTools(server, client2) {
64269
64816
  server.registerTool(
64270
64817
  "gemini_delete_character",
64271
64818
  {
64272
- description: "Delete a saved character from this account's reference library. Generation calls naming it will then fail until it is re-saved.",
64819
+ description: "Delete a saved character from this account's reference library. Generation calls naming it will then fail until it is re-saved. " + confirmNote(),
64273
64820
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
64274
- inputSchema: external_exports.object({ name: NAME_FIELD, confirm: schemaConfirm })
64821
+ inputSchema: external_exports.object({ name: NAME_FIELD, confirmToken: confirmTokenParam })
64275
64822
  },
64276
- async (args) => {
64277
- const gate = previewUnlessConfirmed(args.confirm, "Delete a saved character (its reference image and description)", "DELETE", `characters/${args.name}`);
64823
+ async (args, extra) => {
64824
+ const gate = await confirmWrite(extra, {
64825
+ tool: "gemini_delete_character",
64826
+ action: "character.delete",
64827
+ message: "Review and confirm this deletion:",
64828
+ description: "Delete a saved character (its reference image and description)",
64829
+ method: "DELETE",
64830
+ path: `characters/${args.name}`,
64831
+ target: args.name,
64832
+ payload: { name: args.name },
64833
+ confirmToken: args.confirmToken
64834
+ });
64278
64835
  if (gate) return gate;
64279
64836
  const existed = await library.deleteCharacter(args.name);
64280
64837
  if (!existed) {
@@ -64338,12 +64895,22 @@ function registerLibraryTools(server, client2) {
64338
64895
  server.registerTool(
64339
64896
  "gemini_delete_style",
64340
64897
  {
64341
- description: "Delete a saved style preset from this account's reference library.",
64898
+ description: "Delete a saved style preset from this account's reference library. " + confirmNote(),
64342
64899
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
64343
- inputSchema: external_exports.object({ name: NAME_FIELD, confirm: schemaConfirm })
64900
+ inputSchema: external_exports.object({ name: NAME_FIELD, confirmToken: confirmTokenParam })
64344
64901
  },
64345
- async (args) => {
64346
- const gate = previewUnlessConfirmed(args.confirm, "Delete a saved style preset", "DELETE", `styles/${args.name}`);
64902
+ async (args, extra) => {
64903
+ const gate = await confirmWrite(extra, {
64904
+ tool: "gemini_delete_style",
64905
+ action: "style.delete",
64906
+ message: "Review and confirm this deletion:",
64907
+ description: "Delete a saved style preset",
64908
+ method: "DELETE",
64909
+ path: `styles/${args.name}`,
64910
+ target: args.name,
64911
+ payload: { name: args.name },
64912
+ confirmToken: args.confirmToken
64913
+ });
64347
64914
  if (gate) return gate;
64348
64915
  const existed = await library.deleteStyle(args.name);
64349
64916
  if (!existed) {