gogcli-mcp 4.3.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -34206,14 +34206,14 @@ function serveStdio(factory, options = {}) {
34206
34206
  return false;
34207
34207
  };
34208
34208
  const answerLegacyRejection = (request, reason, requestedVersion) => {
34209
- const rejection2 = modernOnlyStrictRejection({
34209
+ const rejection3 = modernOnlyStrictRejection({
34210
34210
  kind: "legacy",
34211
34211
  reason,
34212
34212
  ...requestedVersion !== void 0 && { requestedVersion }
34213
34213
  }, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
34214
- if (rejection2 === void 0) return Promise.resolve();
34215
- reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection2.cell}): ${rejection2.message}`));
34216
- return writeErrorResponse(request.id, rejection2.code, rejection2.message, rejection2.data);
34214
+ if (rejection3 === void 0) return Promise.resolve();
34215
+ reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection3.cell}): ${rejection3.message}`));
34216
+ return writeErrorResponse(request.id, rejection3.code, rejection3.message, rejection3.data);
34217
34217
  };
34218
34218
  const onInstanceClosed = (channel) => {
34219
34219
  if (closing || channel === discarding) return;
@@ -34554,10 +34554,8 @@ function errorResult(message) {
34554
34554
 
34555
34555
  // ../../node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
34556
34556
  import { createHash, createHmac, timingSafeEqual } from "node:crypto";
34557
- var DEFAULT_REQUEST_KEY = "confirmation";
34558
- var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
34559
- var 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";
34560
- var STATE_PREFIX = "mcpu.confirm.v1.";
34557
+
34558
+ // ../../node_modules/@chrischall/mcp-utils/dist/server/canonical.js
34561
34559
  function canonicalJson(value) {
34562
34560
  if (value === null || typeof value === "string" || typeof value === "boolean")
34563
34561
  return JSON.stringify(value);
@@ -34568,7 +34566,7 @@ function canonicalJson(value) {
34568
34566
  if (typeof value === "bigint")
34569
34567
  return `{"$bigint":"${value.toString()}"}`;
34570
34568
  if (typeof value === "function" || typeof value === "symbol") {
34571
- throw new TypeError(`requireConfirmation: cannot bind a ${typeof value} argument value.`);
34569
+ throw new TypeError(`confirmation: cannot bind a ${typeof value} argument value.`);
34572
34570
  }
34573
34571
  if (Array.isArray(value))
34574
34572
  return `[${value.map(canonicalJson).join(",")}]`;
@@ -34590,7 +34588,7 @@ function canonicalJson(value) {
34590
34588
  }
34591
34589
  const proto = Object.getPrototypeOf(value);
34592
34590
  if (proto !== Object.prototype && proto !== null) {
34593
- throw new TypeError("requireConfirmation: cannot bind a non-plain object argument value (class instance).");
34591
+ throw new TypeError("confirmation: cannot bind a non-plain object argument value (class instance).");
34594
34592
  }
34595
34593
  const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
34596
34594
  return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
@@ -34598,6 +34596,12 @@ function canonicalJson(value) {
34598
34596
  function escapeKey(key) {
34599
34597
  return key.startsWith("$") ? `$${key}` : key;
34600
34598
  }
34599
+
34600
+ // ../../node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
34601
+ var DEFAULT_REQUEST_KEY = "confirmation";
34602
+ var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
34603
+ var 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";
34604
+ var STATE_PREFIX = "mcpu.confirm.v1.";
34601
34605
  function bindingKey(binding) {
34602
34606
  if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
34603
34607
  throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
@@ -34700,6 +34704,264 @@ ${JSON.stringify(preview, null, 2)}`,
34700
34704
  });
34701
34705
  }
34702
34706
 
34707
+ // ../../node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js
34708
+ import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
34709
+ function createSpentTokenStore() {
34710
+ const spent2 = /* @__PURE__ */ new Map();
34711
+ return {
34712
+ has: (nonce) => spent2.has(nonce),
34713
+ add: (nonce, exp) => {
34714
+ spent2.set(nonce, exp);
34715
+ },
34716
+ prune: (now) => {
34717
+ for (const [nonce, exp] of spent2)
34718
+ if (exp < now)
34719
+ spent2.delete(nonce);
34720
+ },
34721
+ clear: () => spent2.clear(),
34722
+ get size() {
34723
+ return spent2.size;
34724
+ }
34725
+ };
34726
+ }
34727
+ var PROCESS_SPENT = createSpentTokenStore();
34728
+ var PREFIX = "mcpu.token.v1.";
34729
+ var DEFAULT_TTL_SECONDS = 600;
34730
+ function tokenKey(key) {
34731
+ const bytes = typeof key === "string" ? Buffer.from(key, "utf8") : Buffer.from(key);
34732
+ if (bytes.length < 32)
34733
+ throw new RangeError("confirm token: key must be at least 32 bytes.");
34734
+ return bytes;
34735
+ }
34736
+ function ttlOf(ttlSeconds) {
34737
+ if (ttlSeconds === void 0)
34738
+ return DEFAULT_TTL_SECONDS;
34739
+ if (!(Number.isFinite(ttlSeconds) && ttlSeconds > 0)) {
34740
+ throw new RangeError("confirm token: ttlSeconds must be a finite number greater than 0.");
34741
+ }
34742
+ return ttlSeconds;
34743
+ }
34744
+ function sign(key, body) {
34745
+ return createHmac2("sha256", key).update(`${PREFIX}${body}`).digest();
34746
+ }
34747
+ function hashConfirmPayload(payload) {
34748
+ return createHash2("sha256").update(canonicalJson(payload)).digest("base64url");
34749
+ }
34750
+ function issueConfirmToken(key, binding, options = {}) {
34751
+ const k = tokenKey(key);
34752
+ const now = options.now ?? Date.now();
34753
+ const exp = now + ttlOf(options.ttlSeconds) * 1e3;
34754
+ const claims = {
34755
+ t: binding.tool,
34756
+ ...binding.account === void 0 ? {} : { a: binding.account },
34757
+ g: binding.target,
34758
+ ...binding.revision === void 0 ? {} : { r: binding.revision },
34759
+ h: binding.payloadHash,
34760
+ exp,
34761
+ n: randomBytes(16).toString("base64url")
34762
+ };
34763
+ const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
34764
+ return { token: `${PREFIX}${body}.${sign(k, body).toString("base64url")}`, expiresAt: new Date(exp).toISOString() };
34765
+ }
34766
+ function parseClaims(body) {
34767
+ try {
34768
+ const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
34769
+ return claims && typeof claims === "object" ? claims : void 0;
34770
+ } catch {
34771
+ return void 0;
34772
+ }
34773
+ }
34774
+ function verifyConfirmToken(key, token, binding, options = {}) {
34775
+ const k = tokenKey(key);
34776
+ const now = options.now ?? Date.now();
34777
+ const spent2 = options.spent ?? PROCESS_SPENT;
34778
+ spent2.prune(now);
34779
+ if (!token.startsWith(PREFIX))
34780
+ return { ok: false, error: "TOKEN_INVALID" };
34781
+ const rest = token.slice(PREFIX.length);
34782
+ const dot = rest.indexOf(".");
34783
+ const body = dot < 0 ? "" : rest.slice(0, dot);
34784
+ const mac3 = Buffer.from(dot < 0 ? "" : rest.slice(dot + 1), "base64url");
34785
+ const expected = sign(k, body);
34786
+ if (!body || mac3.length !== expected.length || !timingSafeEqual2(mac3, expected))
34787
+ return { ok: false, error: "TOKEN_INVALID" };
34788
+ const claims = parseClaims(body);
34789
+ if (!claims)
34790
+ return { ok: false, error: "TOKEN_INVALID" };
34791
+ if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
34792
+ return { ok: false, error: "TOKEN_INVALID" };
34793
+ }
34794
+ if (spent2.has(claims.n))
34795
+ return { ok: false, error: "TOKEN_REUSED" };
34796
+ if (now > claims.exp)
34797
+ return { ok: false, error: "TOKEN_EXPIRED" };
34798
+ if (claims.r !== binding.revision)
34799
+ return { ok: false, error: "DRAFT_CHANGED", reason: "revision-changed" };
34800
+ if (claims.h !== binding.payloadHash)
34801
+ return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
34802
+ spent2.add(claims.n, claims.exp);
34803
+ return { ok: true };
34804
+ }
34805
+ var CONFIRM_TOKEN_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
34806
+ var 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.`);
34807
+ var TOKEN_ERROR_NOTE = {
34808
+ 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.",
34809
+ 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.",
34810
+ 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."
34811
+ };
34812
+ var CHANGED_NOTE = {
34813
+ "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.",
34814
+ "payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
34815
+ };
34816
+ function isToolResult(value) {
34817
+ return Array.isArray(value.content);
34818
+ }
34819
+ function rejection2(data) {
34820
+ return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
34821
+ }
34822
+ async function tokenConfirmation(action, fb) {
34823
+ const subject = await fb.subject();
34824
+ if (isToolResult(subject))
34825
+ return subject;
34826
+ const binding = {
34827
+ tool: fb.tool,
34828
+ ...fb.account === void 0 ? {} : { account: fb.account },
34829
+ target: subject.target,
34830
+ ...subject.revision === void 0 ? {} : { revision: subject.revision },
34831
+ payloadHash: hashConfirmPayload(subject.payload)
34832
+ };
34833
+ const phaseOne = () => {
34834
+ const { token, expiresAt } = issueConfirmToken(fb.key, binding, { ttlSeconds: fb.ttlSeconds });
34835
+ return {
34836
+ action,
34837
+ preview: subject.preview,
34838
+ confirmToken: token,
34839
+ expiresAt,
34840
+ ttlSeconds: ttlOf(fb.ttlSeconds),
34841
+ instruction: fb.instruction ?? CONFIRM_TOKEN_INSTRUCTION
34842
+ };
34843
+ };
34844
+ if (!fb.confirmToken) {
34845
+ return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
34846
+ }
34847
+ const verdict = verifyConfirmToken(fb.key, fb.confirmToken, binding, { spent: fb.spent });
34848
+ if (verdict.ok)
34849
+ return void 0;
34850
+ if (verdict.error === "DRAFT_CHANGED") {
34851
+ return rejection2({
34852
+ error: "DRAFT_CHANGED",
34853
+ reason: verdict.reason,
34854
+ note: `${CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
34855
+ ...phaseOne()
34856
+ });
34857
+ }
34858
+ return rejection2({ error: verdict.error, action, note: TOKEN_ERROR_NOTE[verdict.error] });
34859
+ }
34860
+ async function requireConfirmationWithFallback(ctx, options) {
34861
+ const { tokenFallback, ...confirmation } = options;
34862
+ if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
34863
+ return tokenConfirmation(options.action, tokenFallback);
34864
+ }
34865
+ return requireConfirmation(ctx, confirmation);
34866
+ }
34867
+
34868
+ // ../../node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
34869
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
34870
+
34871
+ // ../../node_modules/@chrischall/mcp-utils/dist/config/index.js
34872
+ import { homedir } from "node:os";
34873
+ import { isAbsolute, join, resolve } from "node:path";
34874
+ var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
34875
+ function readEnvVar(key, opts = {}) {
34876
+ const env = opts.env ?? process.env;
34877
+ const raw = env[key];
34878
+ if (typeof raw === "string") {
34879
+ const trimmed = raw.trim();
34880
+ if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
34881
+ return trimmed;
34882
+ }
34883
+ }
34884
+ return opts.default;
34885
+ }
34886
+ var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
34887
+ var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
34888
+ function parseBoolEnv(key, opts = {}) {
34889
+ const fallback = opts.default ?? false;
34890
+ const raw = readEnvVar(key, { env: opts.env });
34891
+ if (raw === void 0)
34892
+ return fallback;
34893
+ const token = raw.toLowerCase();
34894
+ if (TRUE_TOKENS.has(token))
34895
+ return true;
34896
+ if (FALSE_TOKENS.has(token))
34897
+ return false;
34898
+ return fallback;
34899
+ }
34900
+ function expandPath(p) {
34901
+ let expanded = p;
34902
+ if (p === "~") {
34903
+ expanded = homedir();
34904
+ } else if (p.startsWith("~/")) {
34905
+ expanded = join(homedir(), p.slice(2));
34906
+ }
34907
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
34908
+ }
34909
+
34910
+ // ../../node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
34911
+ var MODES = /* @__PURE__ */ new Set(["ask-user", "auto", "refuse"]);
34912
+ var DEFAULT_TTL_SECONDS2 = 600;
34913
+ var warned = /* @__PURE__ */ new Set();
34914
+ function readConfirmMode(env = process.env) {
34915
+ const raw = readEnvVar("MCP_CONFIRM_MODE", { env })?.trim().toLowerCase();
34916
+ if (!raw)
34917
+ return "ask-user";
34918
+ if (MODES.has(raw))
34919
+ return raw;
34920
+ if (!warned.has(raw)) {
34921
+ warned.add(raw);
34922
+ process.stderr.write(`MCP_CONFIRM_MODE="${raw}" is not one of ask-user, auto, refuse; treating it as refuse.
34923
+ `);
34924
+ }
34925
+ return "refuse";
34926
+ }
34927
+ function confirmTtlFromEnv(env = process.env) {
34928
+ const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
34929
+ return raw && /^[1-9]\d*$/.test(raw) ? Number(raw) : DEFAULT_TTL_SECONDS2;
34930
+ }
34931
+ var processKey;
34932
+ function confirmKeyFromEnv(env = process.env) {
34933
+ const secret = readEnvVar("MCP_CONFIRM_SECRET", { env });
34934
+ if (secret)
34935
+ return createHash3("sha256").update(secret, "utf8").digest();
34936
+ processKey ??= randomBytes2(32);
34937
+ return processKey;
34938
+ }
34939
+ var 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.)";
34940
+ var REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
34941
+ function confirmationFromEnv(options) {
34942
+ const { tool, account, confirmToken, subject, instruction, spent: spent2, env = process.env, ...confirmation } = options;
34943
+ const mode = readConfirmMode(env);
34944
+ if (mode === "refuse") {
34945
+ return {
34946
+ ...confirmation,
34947
+ unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${REFUSE_HINT}` : REFUSE_HINT
34948
+ };
34949
+ }
34950
+ return {
34951
+ ...confirmation,
34952
+ tokenFallback: {
34953
+ key: confirmKeyFromEnv(env),
34954
+ tool,
34955
+ ...account === void 0 ? {} : { account },
34956
+ ...confirmToken === void 0 ? {} : { confirmToken },
34957
+ subject,
34958
+ ttlSeconds: confirmTtlFromEnv(env),
34959
+ instruction: mode === "auto" ? CONFIRM_TOKEN_AUTO_INSTRUCTION : instruction ?? CONFIRM_TOKEN_INSTRUCTION,
34960
+ ...spent2 === void 0 ? {} : { spent: spent2 }
34961
+ }
34962
+ };
34963
+ }
34964
+
34703
34965
  // ../../node_modules/@chrischall/mcp-utils/dist/server/index.js
34704
34966
  var SERVER_PROTOCOL_VERSIONS = Object.freeze([
34705
34967
  "2026-07-28",
@@ -34875,45 +35137,6 @@ function runMcp(opts) {
34875
35137
  return handle;
34876
35138
  }
34877
35139
 
34878
- // ../../node_modules/@chrischall/mcp-utils/dist/config/index.js
34879
- import { homedir } from "node:os";
34880
- import { isAbsolute, join, resolve } from "node:path";
34881
- var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
34882
- function readEnvVar(key, opts = {}) {
34883
- const env = opts.env ?? process.env;
34884
- const raw = env[key];
34885
- if (typeof raw === "string") {
34886
- const trimmed = raw.trim();
34887
- if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
34888
- return trimmed;
34889
- }
34890
- }
34891
- return opts.default;
34892
- }
34893
- var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
34894
- var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
34895
- function parseBoolEnv(key, opts = {}) {
34896
- const fallback = opts.default ?? false;
34897
- const raw = readEnvVar(key, { env: opts.env });
34898
- if (raw === void 0)
34899
- return fallback;
34900
- const token = raw.toLowerCase();
34901
- if (TRUE_TOKENS.has(token))
34902
- return true;
34903
- if (FALSE_TOKENS.has(token))
34904
- return false;
34905
- return fallback;
34906
- }
34907
- function expandPath(p) {
34908
- let expanded = p;
34909
- if (p === "~") {
34910
- expanded = homedir();
34911
- } else if (p.startsWith("~/")) {
34912
- expanded = join(homedir(), p.slice(2));
34913
- }
34914
- return isAbsolute(expanded) ? expanded : resolve(expanded);
34915
- }
34916
-
34917
35140
  // ../../node_modules/@chrischall/mcp-utils/dist/fs/confine.js
34918
35141
  import { existsSync, realpathSync } from "node:fs";
34919
35142
  import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
@@ -34995,9 +35218,9 @@ function offsetAt(instant, tz) {
34995
35218
  const w = wallPartsIn(instant, tz);
34996
35219
  const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
34997
35220
  const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
34998
- const sign = minutes < 0 ? "-" : "+";
35221
+ const sign2 = minutes < 0 ? "-" : "+";
34999
35222
  const abs = Math.abs(minutes);
35000
- return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
35223
+ return `${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
35001
35224
  }
35002
35225
  function wallPartsIn(instant, tz) {
35003
35226
  const parts = new Intl.DateTimeFormat("en-US", {
@@ -35843,13 +36066,108 @@ function assertNotBoth(inlineParam, fileParam, inlineValue, fileValue) {
35843
36066
  }
35844
36067
  }
35845
36068
 
36069
+ // src/dispatch-confirmation.ts
36070
+ import { createHash as createHash4 } from "node:crypto";
36071
+ import { readFileSync } from "node:fs";
36072
+
36073
+ // src/send-confirm-token.ts
36074
+ var spent = createSpentTokenStore();
36075
+ function confirmSpentStore() {
36076
+ return spent;
36077
+ }
36078
+
36079
+ // src/dispatch-confirmation.ts
36080
+ var BODY_PREVIEW_MAX = 2048;
36081
+ function bodyPreview(text) {
36082
+ if (!text) return void 0;
36083
+ if (text.length <= BODY_PREVIEW_MAX) return text;
36084
+ return `${text.slice(0, BODY_PREVIEW_MAX)}\u2026 [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
36085
+ }
36086
+ function attachmentNames(paths, inline) {
36087
+ return [...paths ?? [], ...(inline ?? []).map((a) => a.filename)];
36088
+ }
36089
+ function attachmentDetails(paths, inline) {
36090
+ const out = [];
36091
+ for (const path of paths ?? []) {
36092
+ let bytes;
36093
+ try {
36094
+ bytes = readFileSync(path);
36095
+ } catch {
36096
+ bytes = void 0;
36097
+ }
36098
+ out.push(bytes ? { name: path, size: bytes.length, sha256: createHash4("sha256").update(bytes).digest("hex") } : { name: path, size: null });
36099
+ }
36100
+ for (const a of inline ?? []) {
36101
+ out.push({
36102
+ name: a.filename,
36103
+ size: Buffer.from(a.contentBase64, "base64").length,
36104
+ sha256: createHash4("sha256").update(a.contentBase64).digest("hex")
36105
+ });
36106
+ }
36107
+ return out;
36108
+ }
36109
+ function senderPreview(account, from) {
36110
+ return from ?? account ?? readEnvVar("GOG_ACCOUNT") ?? "the gog account's default address";
36111
+ }
36112
+ function attachmentPreview(details) {
36113
+ return details.map(({ name, size }) => ({ name, size }));
36114
+ }
36115
+ var CONFIRM_SEND_INSTRUCTION = "Show this preview to the user verbatim and send only after they explicitly approve in chat. Then call again with confirmToken.";
36116
+ var CONFIRM_ACTION_INSTRUCTION = CONFIRM_TOKEN_INSTRUCTION;
36117
+ var CONFIRM_FALLBACK_DESCRIPTION = ' If the client cannot show that prompt (no MCP elicitation, e.g. claude.ai), a two-step flow applies instead: call WITHOUT confirmToken and nothing is sent or changed \u2014 the result has status "confirmation-required", the full preview and a confirmToken. Follow its instruction (by default: show the preview to the user verbatim and only after they explicitly approve it in chat, call again with the SAME arguments plus confirmToken). The tool re-reads what it would act on and refuses (DRAFT_CHANGED, with a fresh preview and token) if it changed; TOKEN_EXPIRED / TOKEN_REUSED / TOKEN_INVALID also do nothing. A server set to MCP_CONFIRM_MODE=refuse refuses instead.';
36118
+ function gatedElsewhere(what, via, does, tool) {
36119
+ return `${what} ${does} and is not available through ${via}. Use ${tool}, which asks the user to confirm.`;
36120
+ }
36121
+ function hasCommandWord(args, words) {
36122
+ return args.find((a) => words.has(a.toLowerCase()));
36123
+ }
36124
+ async function requireDispatchConfirmation(ctx, options) {
36125
+ const { action, fallback } = options;
36126
+ const confirmation = {
36127
+ action,
36128
+ message: options.message,
36129
+ details: options.details,
36130
+ confirmationLabel: options.confirmationLabel,
36131
+ ...options.unsupportedNote ? { unsupportedNote: options.unsupportedNote } : {}
36132
+ };
36133
+ if (!fallback) return requireConfirmationWithFallback(ctx, confirmation);
36134
+ return requireConfirmationWithFallback(ctx, confirmationFromEnv({
36135
+ ...confirmation,
36136
+ tool: fallback.tool,
36137
+ account: fallback.account ?? readEnvVar("GOG_ACCOUNT") ?? "",
36138
+ confirmToken: fallback.confirmToken,
36139
+ subject: fallback.subject,
36140
+ instruction: fallback.instruction ?? CONFIRM_ACTION_INSTRUCTION,
36141
+ spent: confirmSpentStore()
36142
+ }));
36143
+ }
36144
+ function resultText(result) {
36145
+ const first = result.content[0];
36146
+ return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
36147
+ }
36148
+
35846
36149
  // src/tools/api.ts
35847
36150
  var GMAIL_API_BLOCKED = /(?:\.send$|forwarding|filters\.create|filters\.update|delegates\.create)/i;
36151
+ var DISPATCH_API_BLOCKED = {
36152
+ chat: [{ method: /(?:^|\.)spaces\.messages\.create$/i, does: "posts a Chat message", tool: "gog_chat_messages_send / gog_chat_dm_send" }],
36153
+ drive: [{ method: /(?:^|\.)permissions\.(?:create|update)$/i, does: "grants access to a file", tool: "gog_drive_share" }],
36154
+ classroom: [
36155
+ { method: /(?:^|\.)courses\.announcements\.create$/i, does: "posts to a class", tool: "gog_classroom_announcements_create" },
36156
+ { method: /(?:^|\.)invitations\.create$/i, does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
36157
+ ],
36158
+ calendar: [
36159
+ { method: /(?:^|\.)events\.(?:insert|import|quickadd)$/i, does: "can put an event on guests' calendars", tool: "gog_calendar_create" },
36160
+ { method: /(?:^|\.)events\.(?:update|patch)$/i, does: "can change what guests see", tool: "gog_calendar_update / gog_calendar_respond" }
36161
+ ]
36162
+ };
35848
36163
  function refusedApiCall(api, method) {
35849
- if (api.trim().toLowerCase() === "gmail" && GMAIL_API_BLOCKED.test(method.trim())) {
36164
+ const name = api.trim().toLowerCase();
36165
+ const m = method.trim();
36166
+ if (name === "gmail" && GMAIL_API_BLOCKED.test(m)) {
35850
36167
  return `gmail ${method} is not available through gog_api_call: it sends or forwards mail. Use gog_gmail_send / gog_gmail_drafts_send (which ask the user to confirm) or the dedicated gog_gmail_* tool.`;
35851
36168
  }
35852
- return void 0;
36169
+ const hit = (Object.hasOwn(DISPATCH_API_BLOCKED, name) ? DISPATCH_API_BLOCKED[name] : []).find((b) => b.method.test(m));
36170
+ return hit ? gatedElsewhere(`${name} ${m}`, "gog_api_call", hit.does, hit.tool) : void 0;
35853
36171
  }
35854
36172
  function registerApiTools(server) {
35855
36173
  server.registerTool("gog_api_list", {
@@ -35879,7 +36197,7 @@ function registerApiTools(server) {
35879
36197
  return runOrDiagnose(args, { account });
35880
36198
  });
35881
36199
  server.registerTool("gog_api_call", {
35882
- description: "Call any Discovery-described Google API method directly \u2014 an escape hatch for endpoints gog has no dedicated tool for. Find the exact api/version/method/params with gog_api_describe first. Read methods (GET/LIST) run as-is. Mutating methods (POST/PUT/PATCH/DELETE) are refused unless you set allowWrite=true \u2014 keep it false to preview, or set dryRun=true to print the intended request without sending it. Gmail send and forwarding methods (users.messages.send, users.drafts.send, forwarding/auto-forwarding, filters, delegates) are refused \u2014 use the dedicated gog_gmail_* tools, which ask the user to confirm.",
36200
+ description: "Call any Discovery-described Google API method directly \u2014 an escape hatch for endpoints gog has no dedicated tool for. Find the exact api/version/method/params with gog_api_describe first. Read methods (GET/LIST) run as-is. Mutating methods (POST/PUT/PATCH/DELETE) are refused unless you set allowWrite=true \u2014 keep it false to preview, or set dryRun=true to print the intended request without sending it. Gmail send and forwarding methods (users.messages.send, users.drafts.send, forwarding/auto-forwarding, filters, delegates) are refused \u2014 use the dedicated gog_gmail_* tools, which ask the user to confirm. So are the other methods a dedicated tool asks about: Chat spaces.messages.create, Drive permissions.create/update, Classroom courses.announcements.create and invitations.create, and Calendar events.insert/import/quickAdd/update/patch.",
35883
36201
  annotations: { destructiveHint: true },
35884
36202
  inputSchema: external_exports.object({
35885
36203
  api: external_exports.string().describe("Discovery API name (e.g. drive, gmail, calendar)"),
@@ -36162,6 +36480,54 @@ function registerAuthTools(server) {
36162
36480
  }
36163
36481
 
36164
36482
  // src/tools/calendar.ts
36483
+ var CALENDAR_GATED = {
36484
+ create: "gog_calendar_create",
36485
+ add: "gog_calendar_create",
36486
+ new: "gog_calendar_create",
36487
+ update: "gog_calendar_update",
36488
+ edit: "gog_calendar_update",
36489
+ set: "gog_calendar_update",
36490
+ respond: "gog_calendar_respond",
36491
+ rsvp: "gog_calendar_respond",
36492
+ reply: "gog_calendar_respond"
36493
+ };
36494
+ function vetCalendarRun(subcommand, _args) {
36495
+ const sub = subcommand.toLowerCase();
36496
+ const tool = Object.hasOwn(CALENDAR_GATED, sub) ? CALENDAR_GATED[sub] : void 0;
36497
+ return tool ? gatedElsewhere(`gog calendar ${sub}`, "gog_calendar_run", "can change what guests see", tool) : void 0;
36498
+ }
36499
+ function parseAttendees(value) {
36500
+ if (!value) return [];
36501
+ return value.split(",").map((entry) => entry.split(";")).filter((parts) => !parts.slice(1).some((m) => m.trim().toLowerCase() === "resource")).map((parts) => parts[0].trim()).filter(Boolean);
36502
+ }
36503
+ function timeOf(t) {
36504
+ const v = t?.dateTime ?? t?.date;
36505
+ return typeof v === "string" ? v : void 0;
36506
+ }
36507
+ function eventSnapshot(raw) {
36508
+ let event;
36509
+ try {
36510
+ event = JSON.parse(raw)?.event;
36511
+ } catch {
36512
+ event = void 0;
36513
+ }
36514
+ const str = (v) => typeof v === "string" ? v : void 0;
36515
+ return {
36516
+ summary: str(event?.summary),
36517
+ start: timeOf(event?.start),
36518
+ end: timeOf(event?.end),
36519
+ organizer: str(event?.organizer?.email),
36520
+ // Guests are the OTHER people: not this account's own attendee row, not rooms.
36521
+ guests: (Array.isArray(event?.attendees) ? event.attendees : []).filter((a) => a.self !== true && a.resource !== true && typeof a.email === "string").map((a) => a.email),
36522
+ etag: str(event?.etag)
36523
+ };
36524
+ }
36525
+ async function readEvent(calendarId, eventId, account) {
36526
+ const got = await runOrDiagnose(["calendar", "event", pos(calendarId), pos(eventId)], { account });
36527
+ if (got.isError) return { error: got };
36528
+ return { event: eventSnapshot(resultText(got)) };
36529
+ }
36530
+ var CALENDAR_CONFIRM_LABEL = "Confirm that this change, which other people will see, should be made now.";
36165
36531
  var reminderParams = {
36166
36532
  reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
36167
36533
  `Reminders as method:duration, e.g. ["popup:30m", "email:1d"]. Method is popup or email; duration accepts m/h/d (max 40320 minutes = 4 weeks). Google allows at most 5. These REPLACE the event's reminders \u2014 on update, pass an EMPTY array to drop custom reminders and go back to the calendar's defaults. Cannot be combined with noReminders.`
@@ -36249,7 +36615,7 @@ function registerCalendarTools(server) {
36249
36615
  return runOrDiagnose(args, { account });
36250
36616
  });
36251
36617
  server.registerTool("gog_calendar_create", {
36252
- description: "Create a calendar event. Set withZoom=true to attach a Zoom meeting (requires Zoom S2S OAuth setup via gog_zoom_auth_setup; the join URL + meeting ID + passcode are appended to the event description \u2014 Google rejects native conference card writes from non-Workspace-Marketplace OAuth clients).",
36618
+ description: "Create a calendar event. Set withZoom=true to attach a Zoom meeting (requires Zoom S2S OAuth setup via gog_zoom_auth_setup; the join URL + meeting ID + passcode are appended to the event description \u2014 Google rejects native conference card writes from non-Workspace-Marketplace OAuth clients). With attendees, the event lands on other people's calendars (no invitation email is sent), so the MCP host is asked to show the user a confirmation prompt with the guests, time and details first; a guest-free event is created without asking." + CONFIRM_FALLBACK_DESCRIPTION,
36253
36619
  annotations: { destructiveHint: false },
36254
36620
  inputSchema: external_exports.object({
36255
36621
  calendarId: external_exports.string().describe('Calendar ID (use "primary" for the default calendar)'),
@@ -36263,9 +36629,10 @@ function registerCalendarTools(server) {
36263
36629
  timezone: external_exports.string().optional().describe("IANA timezone metadata applied to from/to (e.g. America/New_York). Sets both start and end timezone unless start/end timezone are overridden."),
36264
36630
  withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)"),
36265
36631
  ...reminderParams,
36266
- account: accountParam
36632
+ account: accountParam,
36633
+ confirmToken: confirmTokenParam
36267
36634
  })
36268
- }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
36635
+ }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account, confirmToken }, ctx) => {
36269
36636
  const args = ["calendar", "create", pos(calendarId), `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
36270
36637
  if (description) args.push(`--description=${description}`);
36271
36638
  if (location) args.push(`--location=${location}`);
@@ -36274,10 +36641,28 @@ function registerCalendarTools(server) {
36274
36641
  if (timezone) args.push(`--timezone=${timezone}`);
36275
36642
  if (withZoom) args.push("--with-zoom");
36276
36643
  pushReminderFlags(args, { reminders, noReminders });
36644
+ const guests = parseAttendees(attendees);
36645
+ if (guests.length > 0) {
36646
+ const event = { calendarId, summary, from, to, allDay: Boolean(allDay), timezone, location, description, guests, attendees, withZoom: Boolean(withZoom) };
36647
+ const confirmation = await requireDispatchConfirmation(ctx, {
36648
+ action: "calendar.create",
36649
+ message: "Review and confirm this event, which will appear on the guests' calendars:",
36650
+ confirmationLabel: CALENDAR_CONFIRM_LABEL,
36651
+ details: event,
36652
+ unsupportedNote: "Create it without attendees instead; the user can add the guests in Google Calendar.",
36653
+ fallback: {
36654
+ tool: "gog_calendar_create",
36655
+ account,
36656
+ confirmToken,
36657
+ subject: () => ({ target: calendarId, payload: event, preview: event })
36658
+ }
36659
+ });
36660
+ if (confirmation) return confirmation;
36661
+ }
36277
36662
  return runOrDiagnose(args, { account });
36278
36663
  });
36279
36664
  server.registerTool("gog_calendar_update", {
36280
- description: "Update an existing calendar event. Zoom: withZoom adds a Zoom meeting, regenerateZoom replaces the existing one, removeZoom strips it. removeMeet clears the event's Google Meet conference data (e.g. before attaching another provider). Conference flags are independent \u2014 use one per call.",
36665
+ description: "Update an existing calendar event. Zoom: withZoom adds a Zoom meeting, regenerateZoom replaces the existing one, removeZoom strips it. removeMeet clears the event's Google Meet conference data (e.g. before attaching another provider). Conference flags are independent \u2014 use one per call. A change guests can see (time, title, place, description, attendees, attachments, conferencing) on an event that has guests \u2014 or gains them \u2014 reads the event and asks the MCP host to show the user a confirmation prompt with the event as it stands and the change; no invitation email is sent. Reminder-only changes and guest-free events are updated without asking." + CONFIRM_FALLBACK_DESCRIPTION,
36281
36666
  annotations: { destructiveHint: false },
36282
36667
  inputSchema: external_exports.object({
36283
36668
  calendarId: external_exports.string().describe("Calendar ID"),
@@ -36295,9 +36680,10 @@ function registerCalendarTools(server) {
36295
36680
  removeZoom: external_exports.boolean().optional().describe("Remove the event's Zoom video conference"),
36296
36681
  removeMeet: external_exports.boolean().optional().describe("Remove the event's Google Meet video conference (clears conference data only)"),
36297
36682
  ...reminderParams,
36298
- account: accountParam
36683
+ account: accountParam,
36684
+ confirmToken: confirmTokenParam
36299
36685
  })
36300
- }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
36686
+ }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account, confirmToken }, ctx) => {
36301
36687
  const args = ["calendar", "update", pos(calendarId), pos(eventId)];
36302
36688
  if (summary !== void 0) args.push(`--summary=${summary}`);
36303
36689
  if (from !== void 0) args.push(`--from=${from}`);
@@ -36312,6 +36698,45 @@ function registerCalendarTools(server) {
36312
36698
  if (removeZoom) args.push("--remove-zoom");
36313
36699
  if (removeMeet) args.push("--remove-meet");
36314
36700
  pushReminderFlags(args, { reminders, noReminders });
36701
+ const changes = Object.fromEntries(Object.entries({
36702
+ summary,
36703
+ from,
36704
+ to,
36705
+ description,
36706
+ location,
36707
+ attendees,
36708
+ addAttendees,
36709
+ attachments,
36710
+ withZoom,
36711
+ regenerateZoom,
36712
+ removeZoom,
36713
+ removeMeet
36714
+ }).filter(([, v]) => v !== void 0 && v !== false));
36715
+ if (Object.keys(changes).length > 0) {
36716
+ const read = await readEvent(calendarId, eventId, account);
36717
+ if (read.error) return read.error;
36718
+ const { etag, ...current } = read.event;
36719
+ const added = [...parseAttendees(attendees), ...parseAttendees(addAttendees)];
36720
+ if (current.guests.length > 0 || added.length > 0) {
36721
+ const view = { calendarId, eventId, current, changes };
36722
+ const confirmation = await requireDispatchConfirmation(ctx, {
36723
+ action: "calendar.update",
36724
+ message: "Review and confirm this change to an event other people are on:",
36725
+ confirmationLabel: CALENDAR_CONFIRM_LABEL,
36726
+ details: view,
36727
+ unsupportedNote: "Ask the user to make this change in Google Calendar.",
36728
+ fallback: {
36729
+ tool: "gog_calendar_update",
36730
+ account,
36731
+ confirmToken,
36732
+ // The etag rotates on any edit, so an event changed elsewhere between
36733
+ // the two phases is DRAFT_CHANGED rather than edited over.
36734
+ subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
36735
+ }
36736
+ });
36737
+ if (confirmation) return confirmation;
36738
+ }
36739
+ }
36315
36740
  return runOrDiagnose(args, { account });
36316
36741
  });
36317
36742
  server.registerTool("gog_calendar_delete", {
@@ -36326,21 +36751,40 @@ function registerCalendarTools(server) {
36326
36751
  return runOrDiagnose(["calendar", "delete", pos(calendarId), pos(eventId), "--force"], { account });
36327
36752
  });
36328
36753
  server.registerTool("gog_calendar_respond", {
36329
- description: "Respond to a calendar event invitation.",
36754
+ description: "Respond to a calendar event invitation. The organizer sees the response, so this reads the event and asks the MCP host to show the user a confirmation prompt with the event, organizer, response and comment; nothing is recorded unless they accept." + CONFIRM_FALLBACK_DESCRIPTION,
36330
36755
  annotations: { destructiveHint: true },
36331
36756
  inputSchema: external_exports.object({
36332
36757
  calendarId: external_exports.string().describe("Calendar ID"),
36333
36758
  eventId: external_exports.string().describe("Event ID"),
36334
36759
  status: external_exports.enum(["accepted", "declined", "tentative"]).describe("Response status"),
36335
36760
  comment: external_exports.string().optional().describe("Optional comment to include with response"),
36336
- account: accountParam
36761
+ account: accountParam,
36762
+ confirmToken: confirmTokenParam
36337
36763
  })
36338
- }, async ({ calendarId, eventId, status, comment, account }) => {
36764
+ }, async ({ calendarId, eventId, status, comment, account, confirmToken }, ctx) => {
36339
36765
  const args = ["calendar", "respond", pos(calendarId), pos(eventId), `--status=${status}`];
36340
36766
  if (comment) args.push(`--comment=${comment}`);
36767
+ const read = await readEvent(calendarId, eventId, account);
36768
+ if (read.error) return read.error;
36769
+ const { etag, ...event } = read.event;
36770
+ const view = { calendarId, eventId, event, response: status, ...comment ? { comment } : {} };
36771
+ const confirmation = await requireDispatchConfirmation(ctx, {
36772
+ action: "calendar.respond",
36773
+ message: "Review and confirm this response, which the organizer will see:",
36774
+ confirmationLabel: "Confirm that this response should be recorded now.",
36775
+ details: view,
36776
+ unsupportedNote: "Ask the user to respond from Google Calendar.",
36777
+ fallback: {
36778
+ tool: "gog_calendar_respond",
36779
+ account,
36780
+ confirmToken,
36781
+ subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
36782
+ }
36783
+ });
36784
+ if (confirmation) return confirmation;
36341
36785
  return runOrDiagnose(args, { account });
36342
36786
  });
36343
- registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"' });
36787
+ registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"', vet: vetCalendarRun });
36344
36788
  }
36345
36789
 
36346
36790
  // src/attachments.ts
@@ -36424,6 +36868,23 @@ function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
36424
36868
  }
36425
36869
 
36426
36870
  // src/tools/chat.ts
36871
+ var CHAT_SEND_WORDS = /* @__PURE__ */ new Set(["send", "create", "post"]);
36872
+ function vetChatRun(subcommand, args) {
36873
+ const sub = subcommand.toLowerCase();
36874
+ if (sub !== "messages" && sub !== "dm") return void 0;
36875
+ const word = hasCommandWord(args, CHAT_SEND_WORDS);
36876
+ if (!word) return void 0;
36877
+ return gatedElsewhere(
36878
+ `gog chat ${sub} ${word.toLowerCase()}`,
36879
+ "gog_chat_run",
36880
+ "posts a message",
36881
+ sub === "dm" ? "gog_chat_dm_send" : "gog_chat_messages_send"
36882
+ );
36883
+ }
36884
+ var CHAT_CONFIRM = {
36885
+ message: "Review and confirm this Google Chat message:",
36886
+ confirmationLabel: "Confirm that this Chat message should be posted now."
36887
+ };
36427
36888
  function registerChatTools(server) {
36428
36889
  const workspaceOnlyNote = ' WORKSPACE ONLY: Google Chat has no API for consumer accounts, so this fails on an @gmail.com account with "chat requires a Google Workspace account". That is the ACCOUNT, not the token \u2014 re-authorizing or adding scopes will not help.';
36429
36890
  const spaceParam = external_exports.string().describe(
@@ -36529,7 +36990,7 @@ function registerChatTools(server) {
36529
36990
  return runOrDiagnose(args, { account });
36530
36991
  });
36531
36992
  server.registerTool("gog_chat_messages_send", {
36532
- description: "Post a message to a Chat space. THIS IS IMMEDIATELY VISIBLE TO EVERYONE IN THE SPACE and cannot be unsent through this tool, so treat it like sending mail, not like saving a draft. Pass `thread` to reply inside an existing conversation (from gog_chat_threads_list or a message's thread field); omit it to start a new one. Text supports Chat's markdown-ish formatting (*bold*, _italic_, `code`)." + workspaceOnlyNote,
36993
+ description: "Post a message to a Chat space. THIS IS IMMEDIATELY VISIBLE TO EVERYONE IN THE SPACE and cannot be unsent through this tool, so treat it like sending mail, not like saving a draft. Pass `thread` to reply inside an existing conversation (from gog_chat_threads_list or a message's thread field); omit it to start a new one. Text supports Chat's markdown-ish formatting (*bold*, _italic_, `code`). Asks the MCP host to show the user a confirmation prompt with the space, thread, text and attachments first; nothing is posted unless they accept." + CONFIRM_FALLBACK_DESCRIPTION + workspaceOnlyNote,
36533
36994
  annotations: { destructiveHint: true },
36534
36995
  inputSchema: external_exports.object({
36535
36996
  space: spaceParam,
@@ -36539,9 +37000,10 @@ function registerChatTools(server) {
36539
37000
  "Attachment file paths, read WHERE GOG RUNS. On a hosted or remote deployment that is not your machine \u2014 use attachInline there instead. Must be inside the server's GOG_FILE_ROOTS directories (default ~/gogcli-mcp-files)."
36540
37001
  ),
36541
37002
  attachInline: attachInlineParam,
36542
- account: accountParam
37003
+ account: accountParam,
37004
+ confirmToken: confirmTokenParam
36543
37005
  })
36544
- }, async ({ space, text, thread, attach, attachInline, account }) => {
37006
+ }, async ({ space, text, thread, attach, attachInline, account, confirmToken }, ctx) => {
36545
37007
  confinePaths(attach, "attach");
36546
37008
  if (text === void 0 && !attach?.length && !attachInline?.length) {
36547
37009
  throw new Error("A Chat message needs text, an attachment, or both.");
@@ -36551,20 +37013,56 @@ function registerChatTools(server) {
36551
37013
  if (thread) args.push(`--thread=${thread}`);
36552
37014
  if (attach) for (const path of attach) args.push(`--attach=${path}`);
36553
37015
  args.push(...inlineAttachmentArgs("attach", attachInline, args));
37016
+ const confirmation = await requireDispatchConfirmation(ctx, {
37017
+ ...CHAT_CONFIRM,
37018
+ action: "chat.message-send",
37019
+ details: { space, thread, textPreview: bodyPreview(text), attachments: attachmentNames(attach, attachInline) },
37020
+ fallback: {
37021
+ tool: "gog_chat_messages_send",
37022
+ account,
37023
+ confirmToken,
37024
+ subject: () => {
37025
+ const attachments = attachmentDetails(attach, attachInline);
37026
+ const from = senderPreview(account);
37027
+ return {
37028
+ target: space,
37029
+ payload: { from, space, thread, text, attachments },
37030
+ preview: { from, space, thread, text, attachments: attachmentPreview(attachments) }
37031
+ };
37032
+ }
37033
+ }
37034
+ });
37035
+ if (confirmation) return confirmation;
36554
37036
  return runOrDiagnose(args, { account });
36555
37037
  });
36556
37038
  server.registerTool("gog_chat_dm_send", {
36557
- description: "Send a direct message to one person by email address, creating the DM space if this is the first message. Delivered immediately and cannot be unsent through this tool. For a room rather than a person, use gog_chat_messages_send." + workspaceOnlyNote,
37039
+ description: "Send a direct message to one person by email address, creating the DM space if this is the first message. Delivered immediately and cannot be unsent through this tool. For a room rather than a person, use gog_chat_messages_send. Asks the MCP host to show the user a confirmation prompt with the recipient and text first; nothing is sent unless they accept." + CONFIRM_FALLBACK_DESCRIPTION + workspaceOnlyNote,
36558
37040
  annotations: { destructiveHint: true },
36559
37041
  inputSchema: external_exports.object({
36560
37042
  email: external_exports.string().describe("Recipient email address"),
36561
37043
  text: external_exports.string().describe("Message text"),
36562
37044
  thread: threadParam,
36563
- account: accountParam
37045
+ account: accountParam,
37046
+ confirmToken: confirmTokenParam
36564
37047
  })
36565
- }, async ({ email: email3, text, thread, account }) => {
37048
+ }, async ({ email: email3, text, thread, account, confirmToken }, ctx) => {
36566
37049
  const args = ["chat", "dm", "send", pos(email3), `--text=${text}`];
36567
37050
  if (thread) args.push(`--thread=${thread}`);
37051
+ const confirmation = await requireDispatchConfirmation(ctx, {
37052
+ ...CHAT_CONFIRM,
37053
+ action: "chat.dm-send",
37054
+ details: { to: email3, thread, textPreview: bodyPreview(text) },
37055
+ fallback: {
37056
+ tool: "gog_chat_dm_send",
37057
+ account,
37058
+ confirmToken,
37059
+ subject: () => {
37060
+ const from = senderPreview(account);
37061
+ return { target: email3, payload: { from, to: email3, thread, text }, preview: { from, to: email3, thread, text } };
37062
+ }
37063
+ }
37064
+ });
37065
+ if (confirmation) return confirmation;
36568
37066
  return runOrDiagnose(args, { account });
36569
37067
  });
36570
37068
  server.registerTool("gog_chat_dm_space", {
@@ -36619,11 +37117,45 @@ function registerChatTools(server) {
36619
37117
  registerRunTool(server, {
36620
37118
  service: "chat",
36621
37119
  examples: '"spaces", "messages", "dm"',
37120
+ vet: vetChatRun,
36622
37121
  note: "Google Chat has no API for consumer accounts: every chat subcommand fails on an @gmail.com account regardless of scopes."
36623
37122
  });
36624
37123
  }
36625
37124
 
36626
37125
  // src/tools/classroom.ts
37126
+ var CLASSROOM_CREATE_WORDS = /* @__PURE__ */ new Set(["create", "add", "new"]);
37127
+ var CLASSROOM_GATED = {
37128
+ announcements: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
37129
+ announcement: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
37130
+ ann: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
37131
+ invitations: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
37132
+ invitation: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
37133
+ invites: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
37134
+ };
37135
+ function vetClassroomRun(subcommand, args) {
37136
+ const sub = subcommand.toLowerCase();
37137
+ const gated = Object.hasOwn(CLASSROOM_GATED, sub) ? CLASSROOM_GATED[sub] : void 0;
37138
+ if (!gated) return void 0;
37139
+ const word = hasCommandWord(args, CLASSROOM_CREATE_WORDS);
37140
+ return word ? gatedElsewhere(`gog classroom ${sub} ${word.toLowerCase()}`, "gog_classroom_run", gated.does, gated.tool) : void 0;
37141
+ }
37142
+ async function readCourse(courseId, account, runner = runOrDiagnose) {
37143
+ const got = await runner(["classroom", "courses", "get", pos(courseId)], { account });
37144
+ if (got.isError) return { error: got };
37145
+ let course;
37146
+ try {
37147
+ course = JSON.parse(resultText(got))?.course;
37148
+ } catch {
37149
+ course = void 0;
37150
+ }
37151
+ return {
37152
+ course: {
37153
+ id: courseId,
37154
+ ...typeof course?.name === "string" ? { name: course.name } : {},
37155
+ ...typeof course?.section === "string" ? { section: course.section } : {}
37156
+ }
37157
+ };
37158
+ }
36627
37159
  function registerClassroomTools(server) {
36628
37160
  server.registerTool("gog_classroom_courses_list", {
36629
37161
  description: "List Google Classroom courses.",
@@ -36906,19 +37438,42 @@ function registerClassroomTools(server) {
36906
37438
  return runOrDiagnose(["classroom", "announcements", "get", pos(courseId), pos(announcementId)], { account });
36907
37439
  });
36908
37440
  server.registerTool("gog_classroom_announcements_create", {
36909
- description: "Create an announcement in a Google Classroom course.",
37441
+ description: "Create an announcement in a Google Classroom course. Unless state is DRAFT (which students cannot see), this reads the course and asks the MCP host to show the user a confirmation prompt with the class, the full text and when it publishes; nothing is posted unless they accept. To stage one without asking, pass state DRAFT." + CONFIRM_FALLBACK_DESCRIPTION,
36910
37442
  annotations: { destructiveHint: true },
36911
37443
  inputSchema: external_exports.object({
36912
37444
  courseId: external_exports.string().describe("Course ID"),
36913
37445
  text: external_exports.string().describe("Announcement text"),
36914
- state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State"),
37446
+ state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State (DRAFT is visible only to teachers and needs no confirmation)"),
36915
37447
  scheduled: external_exports.string().optional().describe("Scheduled publish time"),
36916
- account: accountParam
37448
+ account: accountParam,
37449
+ confirmToken: confirmTokenParam
36917
37450
  })
36918
- }, async ({ courseId, text, state, scheduled, account }) => {
37451
+ }, async ({ courseId, text, state, scheduled, account, confirmToken }, ctx) => {
36919
37452
  const args = ["classroom", "announcements", "create", pos(courseId), `--text=${text}`];
36920
37453
  if (state) args.push(`--state=${state}`);
36921
37454
  if (scheduled) args.push(`--scheduled=${scheduled}`);
37455
+ if (state !== "DRAFT") {
37456
+ const read = await readCourse(courseId, account);
37457
+ if (read.error) return read.error;
37458
+ const publishes = scheduled ? `at ${scheduled}` : "immediately";
37459
+ const confirmation = await requireDispatchConfirmation(ctx, {
37460
+ action: "classroom.announcement-create",
37461
+ message: "Review and confirm this Classroom announcement:",
37462
+ confirmationLabel: "Confirm that this announcement should be posted to the class.",
37463
+ details: { course: read.course, publishes, textPreview: bodyPreview(text) },
37464
+ unsupportedNote: "Create it with state DRAFT instead; the user can review and post it from Classroom.",
37465
+ fallback: {
37466
+ tool: "gog_classroom_announcements_create",
37467
+ account,
37468
+ confirmToken,
37469
+ subject: () => {
37470
+ const view = { course: read.course, publishes, text };
37471
+ return { target: courseId, payload: view, preview: view };
37472
+ }
37473
+ }
37474
+ });
37475
+ if (confirmation) return confirmation;
37476
+ }
36922
37477
  return runOrDiagnose(args, { account });
36923
37478
  });
36924
37479
  server.registerTool("gog_classroom_topics_list", {
@@ -37008,6 +37563,7 @@ function registerClassroomTools(server) {
37008
37563
  registerRunTool(server, {
37009
37564
  service: "classroom",
37010
37565
  examples: '"guardians", "materials", "guardian-invitations"',
37566
+ vet: vetClassroomRun,
37011
37567
  note: "Covers anything not wrapped by the dedicated tools (guardians, guardian-invitations, materials, coursework assignees, announcement assignees, etc.)."
37012
37568
  });
37013
37569
  }
@@ -37168,7 +37724,18 @@ function registerDocsTools(server) {
37168
37724
  }
37169
37725
 
37170
37726
  // src/tools/drive.ts
37727
+ function vetDriveRun(subcommand, _args) {
37728
+ return subcommand.toLowerCase() === "share" ? gatedElsewhere("gog drive share", "gog_drive_run", "grants access to a file", "gog_drive_share") : void 0;
37729
+ }
37171
37730
  var GOOGLE_DOC_MIME = "application/vnd.google-apps.document";
37731
+ function shareTargetMeta(raw) {
37732
+ try {
37733
+ const { name, mimeType } = fileMeta(raw);
37734
+ return { name, mimeType };
37735
+ } catch {
37736
+ return {};
37737
+ }
37738
+ }
37172
37739
  function fileMeta(raw) {
37173
37740
  const parsed = JSON.parse(raw);
37174
37741
  const f = parsed.file ?? parsed;
@@ -37291,7 +37858,7 @@ function registerDriveTools(server) {
37291
37858
  return runOrDiagnose(args, { account });
37292
37859
  });
37293
37860
  server.registerTool("gog_drive_share", {
37294
- description: "Share a Google Drive file or folder.",
37861
+ description: "Share a Google Drive file or folder. Granting access is the risk even though no email is sent (gog does not notify by default), so this reads the file and asks the MCP host to show the user a confirmation prompt naming it, who gets access and with what role; nothing is shared unless they accept." + CONFIRM_FALLBACK_DESCRIPTION,
37295
37862
  annotations: { destructiveHint: true },
37296
37863
  inputSchema: external_exports.object({
37297
37864
  fileId: external_exports.string().describe("File or folder ID"),
@@ -37299,9 +37866,35 @@ function registerDriveTools(server) {
37299
37866
  email: external_exports.string().optional().describe("User email (required when to=user)"),
37300
37867
  domain: external_exports.string().optional().describe("Domain (required when to=domain)"),
37301
37868
  role: external_exports.enum(["reader", "writer"]).optional().describe("Permission role (default: reader)"),
37302
- account: accountParam
37869
+ account: accountParam,
37870
+ confirmToken: confirmTokenParam
37303
37871
  })
37304
- }, async ({ fileId, to, email: email3, domain: domain2, role, account }) => {
37872
+ }, async ({ fileId, to, email: email3, domain: domain2, role, account, confirmToken }, ctx) => {
37873
+ const got = await runOrDiagnose(["drive", "get", pos(fileId)], { account });
37874
+ if (got.isError) return got;
37875
+ const file2 = { id: fileId, ...shareTargetMeta(resultText(got)) };
37876
+ const grant = {
37877
+ file: file2,
37878
+ to,
37879
+ ...email3 ? { email: email3 } : {},
37880
+ ...domain2 ? { domain: domain2 } : {},
37881
+ role: role ?? "reader",
37882
+ publicLink: to === "anyone"
37883
+ };
37884
+ const confirmation = await requireDispatchConfirmation(ctx, {
37885
+ action: "drive.share",
37886
+ message: to === "anyone" ? "Review and confirm making this file available to ANYONE with the link:" : "Review and confirm this Drive share:",
37887
+ confirmationLabel: "Confirm that this access should be granted now.",
37888
+ details: grant,
37889
+ unsupportedNote: "Ask the user to share it themselves from Google Drive.",
37890
+ fallback: {
37891
+ tool: "gog_drive_share",
37892
+ account,
37893
+ confirmToken,
37894
+ subject: () => ({ target: fileId, payload: grant, preview: grant })
37895
+ }
37896
+ });
37897
+ if (confirmation) return confirmation;
37305
37898
  const args = ["drive", "share", pos(fileId), `--to=${to}`];
37306
37899
  if (email3) args.push(`--email=${email3}`);
37307
37900
  if (domain2) args.push(`--domain=${domain2}`);
@@ -37414,7 +38007,7 @@ function registerDriveTools(server) {
37414
38007
  return diagnose(err);
37415
38008
  }
37416
38009
  });
37417
- registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"' });
38010
+ registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"', vet: vetDriveRun });
37418
38011
  }
37419
38012
 
37420
38013
  // src/gmail-results.ts
@@ -37540,24 +38133,16 @@ var STAGING_TWIN = {
37540
38133
  var UNSUPPORTED_NOTE2 = {
37541
38134
  "gmail.drafts-send": "The draft is still saved: ask the user to review it and send it from Gmail."
37542
38135
  };
37543
- var BODY_PREVIEW_MAX = 2048;
37544
- function bodyPreview(text) {
37545
- if (!text) return void 0;
37546
- if (text.length <= BODY_PREVIEW_MAX) return text;
37547
- return `${text.slice(0, BODY_PREVIEW_MAX)}\u2026 [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
37548
- }
37549
- function attachmentNames(paths, inline) {
37550
- return [...paths ?? [], ...(inline ?? []).map((a) => a.filename)];
37551
- }
37552
- function requireGmailDispatchConfirmation(ctx, op, details) {
38136
+ async function requireGmailDispatchConfirmation(ctx, op, details, fallback) {
37553
38137
  const twin = STAGING_TWIN[op];
37554
38138
  const note = twin ? `Stage it with ${twin} instead; the user can review the draft and send it from Gmail (gog_gmail_drafts_send also asks for confirmation).` : UNSUPPORTED_NOTE2[op];
37555
- return requireConfirmation(ctx, {
38139
+ return requireDispatchConfirmation(ctx, {
37556
38140
  action: op,
37557
38141
  message: op === "gmail.filter-forward" ? "Review and confirm this mail-forwarding filter:" : "Review and confirm this email dispatch:",
37558
38142
  details,
37559
38143
  confirmationLabel: op === "gmail.filter-forward" ? "Confirm that matching mail should be forwarded automatically from now on." : "Confirm that this email should be sent now.",
37560
- ...note ? { unsupportedNote: note } : {}
38144
+ unsupportedNote: note,
38145
+ ...fallback ? { fallback: { instruction: CONFIRM_SEND_INSTRUCTION, ...fallback } } : {}
37561
38146
  });
37562
38147
  }
37563
38148
  var EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/gi;
@@ -37611,10 +38196,6 @@ function logGmailDispatch(tool, recipients, account) {
37611
38196
  process.stderr.write(`${JSON.stringify(event)}
37612
38197
  `);
37613
38198
  }
37614
- function resultText(result) {
37615
- const first = result.content[0];
37616
- return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
37617
- }
37618
38199
 
37619
38200
  // src/tools/gmail.ts
37620
38201
  var replySchema = {
@@ -37662,7 +38243,7 @@ function appendReplyFlags(args, f) {
37662
38243
  if (f.signatureFile) args.push(`--signature-file=${f.signatureFile}`);
37663
38244
  args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
37664
38245
  }
37665
- var sendReplySchema = external_exports.object(replySchema);
38246
+ var sendReplySchema = external_exports.object({ ...replySchema, confirmToken: confirmTokenParam });
37666
38247
  function parseMetadataHeaders(raw) {
37667
38248
  let parsed;
37668
38249
  try {
@@ -37687,12 +38268,69 @@ function computeReplyRecipients(kind, headers, flags) {
37687
38268
  }
37688
38269
  return emails;
37689
38270
  }
37690
- async function sendReply(kind, toolName, messageId, account, flags, ctx) {
38271
+ function metadataThreadId(raw) {
38272
+ try {
38273
+ const threadId = JSON.parse(raw)?.message?.threadId;
38274
+ return typeof threadId === "string" ? threadId : void 0;
38275
+ } catch {
38276
+ return void 0;
38277
+ }
38278
+ }
38279
+ function replyTokenSubject(kind, messageId, account, headers, threadId, flags) {
38280
+ const removed = new Set(extractEmails(...flags.remove ?? []));
38281
+ const keep = (emails) => emails.filter((e) => !removed.has(e));
38282
+ const to = keep(extractEmails(headers.from, ...kind === "reply-all" ? [headers.to] : [], ...flags.to ?? []));
38283
+ const cc = keep(extractEmails(...kind === "reply-all" ? [headers.cc] : [], ...flags.cc ?? []));
38284
+ const bcc = keep(extractEmails(...flags.bcc ?? []));
38285
+ const attachments = attachmentDetails(flags.attach, flags.attachInline);
38286
+ const subject = flags.subject || (headers.subject ? `Re: ${headers.subject}` : void 0);
38287
+ const from = flags.from ?? (flags.autoFromAddressedAlias ? "the send-as alias the original was addressed to" : senderPreview(account));
38288
+ const localFiles = attachmentDetails([flags.bodyHtmlFile, flags.signatureFile].filter((p) => Boolean(p)), void 0);
38289
+ return {
38290
+ target: messageId,
38291
+ payload: {
38292
+ kind,
38293
+ to,
38294
+ cc,
38295
+ bcc,
38296
+ subject,
38297
+ from,
38298
+ body: flags.body,
38299
+ bodyHtml: flags.bodyHtml,
38300
+ localFiles,
38301
+ signature: Boolean(flags.signature),
38302
+ signatureFrom: flags.signatureFrom,
38303
+ attachments,
38304
+ quote: !flags.noQuote,
38305
+ threadId,
38306
+ inReplyTo: headers.message_id,
38307
+ references: headers.references
38308
+ },
38309
+ preview: {
38310
+ from,
38311
+ to,
38312
+ cc,
38313
+ bcc,
38314
+ subject,
38315
+ body: flags.body,
38316
+ ...flags.bodyHtml ? { bodyHtml: flags.bodyHtml } : {},
38317
+ ...flags.bodyHtmlFile ? { bodyHtmlFile: flags.bodyHtmlFile } : {},
38318
+ ...flags.signature || flags.signatureFrom || flags.signatureFile ? { signature: flags.signatureFile ?? flags.signatureFrom ?? "the Gmail signature of the sending address" } : {},
38319
+ quotesOriginal: !flags.noQuote,
38320
+ attachments: attachmentPreview(attachments),
38321
+ replyingToMessageId: messageId,
38322
+ threadId,
38323
+ inReplyTo: headers.message_id
38324
+ }
38325
+ };
38326
+ }
38327
+ async function sendReply(kind, toolName, messageId, account, flags, ctx, confirmToken) {
37691
38328
  const metaResult = await runOrDiagnose(["gmail", "get", pos(messageId), "--format=metadata"], { account });
37692
38329
  if (metaResult.isError) return metaResult;
37693
- const headers = parseMetadataHeaders(resultText(metaResult));
38330
+ const metaText = resultText(metaResult);
38331
+ const headers = parseMetadataHeaders(metaText);
37694
38332
  const recipients = computeReplyRecipients(kind, headers, flags);
37695
- const confirmation = requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
38333
+ const confirmation = await requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
37696
38334
  messageId,
37697
38335
  recipients,
37698
38336
  recipientCount: recipients.length,
@@ -37703,6 +38341,13 @@ async function sendReply(kind, toolName, messageId, account, flags, ctx) {
37703
38341
  bodyHtmlFile: flags.bodyHtmlFile,
37704
38342
  attachmentCount: (flags.attach?.length ?? 0) + (flags.attachInline?.length ?? 0),
37705
38343
  attachments: attachmentNames(flags.attach, flags.attachInline)
38344
+ }, {
38345
+ tool: toolName,
38346
+ account,
38347
+ confirmToken,
38348
+ // The metadata read above IS the phase-2 re-read: it runs on every call,
38349
+ // so the original's Message-ID/References are fresh here.
38350
+ subject: () => replyTokenSubject(kind, messageId, account, headers, metadataThreadId(metaText), flags)
37706
38351
  });
37707
38352
  if (confirmation) return confirmation;
37708
38353
  const args = ["gmail", kind, pos(messageId)];
@@ -37720,7 +38365,7 @@ function vetGmailRun(subcommand, args) {
37720
38365
  return `gog gmail ${subcommand} can forward or hand over mail and is not available through gog_gmail_run. Use the dedicated gog_gmail_* tool instead.`;
37721
38366
  }
37722
38367
  if (subcommand === "settings") {
37723
- const blocked = args.find((a) => GMAIL_RUN_BLOCKED_SETTINGS.has(a.toLowerCase()));
38368
+ const blocked = hasCommandWord(args, GMAIL_RUN_BLOCKED_SETTINGS);
37724
38369
  if (blocked) {
37725
38370
  return `gog gmail settings ${blocked} can forward or hand over mail and is not available through gog_gmail_run. Use the dedicated gog_gmail_* tool instead.`;
37726
38371
  }
@@ -37779,7 +38424,7 @@ function registerGmailTools(server) {
37779
38424
  return runOrDiagnose(args, { account });
37780
38425
  });
37781
38426
  server.registerTool("gog_gmail_send", {
37782
- description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the recipients, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. Two ways to attach a file: `attach` takes paths READ ON THE GOG SERVER, and `attachInline` takes the bytes themselves. Use attachInline unless you know the file exists on the same machine gog runs on \u2014 on the hosted connector and any remote deployment there is no shared filesystem, so no path you can name resolves there and `attach` will fail with "no such file or directory". When either is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were embedded. NOT the tool for answering a message: replyToMessageId only files this in the right thread \u2014 the subject, recipients and body are entirely yours, and the original is not quoted unless you set quote. Use gog_gmail_reply / gog_gmail_reply_all instead, which inherit all three.',
38427
+ description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the recipients, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. Two ways to attach a file: `attach` takes paths READ ON THE GOG SERVER, and `attachInline` takes the bytes themselves. Use attachInline unless you know the file exists on the same machine gog runs on \u2014 on the hosted connector and any remote deployment there is no shared filesystem, so no path you can name resolves there and `attach` will fail with "no such file or directory". When either is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were embedded. NOT the tool for answering a message: replyToMessageId only files this in the right thread \u2014 the subject, recipients and body are entirely yours, and the original is not quoted unless you set quote. Use gog_gmail_reply / gog_gmail_reply_all instead, which inherit all three.' + CONFIRM_FALLBACK_DESCRIPTION,
37783
38428
  annotations: { destructiveHint: true },
37784
38429
  inputSchema: external_exports.object({
37785
38430
  to: external_exports.string().describe("Recipient(s), comma-separated"),
@@ -37792,9 +38437,10 @@ function registerGmailTools(server) {
37792
38437
  quote: external_exports.boolean().optional().describe("Include the original message quoted below the body. Requires replyToMessageId or threadId. gog quotes by DEFAULT on gmail reply but never on gmail send, so without this a threaded send arrives with the original nowhere in it."),
37793
38438
  attach: external_exports.array(external_exports.string()).optional().describe(`File paths to attach (repeatable), resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on a hosted deployment (e.g. mcp-host) these paths do not exist and the call fails with "no such file or directory" \u2014 use attachInline there. Each file is read on the server, base64-encoded with a MIME type inferred from its extension, and added as a multipart attachment. Must be inside the server's GOG_FILE_ROOTS directories (default ~/gogcli-mcp-files).`),
37794
38439
  attachInline: attachInlineParam,
37795
- account: accountParam
38440
+ account: accountParam,
38441
+ confirmToken: confirmTokenParam
37796
38442
  })
37797
- }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account }, ctx) => {
38443
+ }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account, confirmToken }, ctx) => {
37798
38444
  confinePaths(attach, "attach");
37799
38445
  const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
37800
38446
  if (cc) args.push(`--cc=${cc}`);
@@ -37806,7 +38452,7 @@ function registerGmailTools(server) {
37806
38452
  const inline = inlineAttachmentArgs("attach", attachInline, args);
37807
38453
  args.push(...inline);
37808
38454
  const recipients = extractEmails(to, cc, bcc);
37809
- const confirmation = requireGmailDispatchConfirmation(ctx, "gmail.send", {
38455
+ const confirmation = await requireGmailDispatchConfirmation(ctx, "gmail.send", {
37810
38456
  to,
37811
38457
  cc,
37812
38458
  bcc,
@@ -37819,6 +38465,32 @@ function registerGmailTools(server) {
37819
38465
  quoting: Boolean(quote),
37820
38466
  attachmentCount: (attach?.length ?? 0) + (attachInline?.length ?? 0),
37821
38467
  attachments: attachmentNames(attach, attachInline)
38468
+ }, {
38469
+ tool: "gog_gmail_send",
38470
+ account,
38471
+ confirmToken,
38472
+ // Nothing is stored between the phases: the payload IS the arguments,
38473
+ // so phase 2 must repeat them and any difference is a changed send.
38474
+ subject: () => {
38475
+ const attachments = attachmentDetails(attach, attachInline);
38476
+ const from = senderPreview(account);
38477
+ return {
38478
+ target: replyToMessageId ?? threadId ?? "",
38479
+ payload: { from, to, cc, bcc, subject, body, attachments, threadId, inReplyTo: replyToMessageId, quote: Boolean(quote) },
38480
+ preview: {
38481
+ from,
38482
+ to,
38483
+ cc,
38484
+ bcc,
38485
+ subject,
38486
+ body,
38487
+ attachments: attachmentPreview(attachments),
38488
+ threadId,
38489
+ inReplyTo: replyToMessageId,
38490
+ quotesOriginal: Boolean(quote)
38491
+ }
38492
+ };
38493
+ }
37822
38494
  });
37823
38495
  if (confirmation) return confirmation;
37824
38496
  const result = await runOrDiagnose(args, { account });
@@ -37826,22 +38498,22 @@ function registerGmailTools(server) {
37826
38498
  return result;
37827
38499
  });
37828
38500
  server.registerTool("gog_gmail_reply", {
37829
- description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipient, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. To STAGE a reply instead of sending it, use gog_gmail_drafts_reply (gogcli-mcp-gmail only), which never needs confirmation. Reply to a Gmail message (goes to the original sender only). USE THIS, not gog_gmail_send, whenever you are answering a message: it threads off the original AND inherits its "Re:" subject and quotes its body below yours, which gog_gmail_send does not \u2014 a send with replyToMessageId lands in the right thread but reads as a brand-new message, with the original nowhere in it. To answer every participant use gog_gmail_reply_all. The gogcli-mcp-gmail package adds two more routes with the same composition: gog_gmail_autoreply to reply across every message matching a query, and gog_gmail_drafts_reply to stage this exact reply as a draft instead of sending it.',
38501
+ description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipient, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. To STAGE a reply instead of sending it, use gog_gmail_drafts_reply (gogcli-mcp-gmail only), which never needs confirmation. Reply to a Gmail message (goes to the original sender only). USE THIS, not gog_gmail_send, whenever you are answering a message: it threads off the original AND inherits its "Re:" subject and quotes its body below yours, which gog_gmail_send does not \u2014 a send with replyToMessageId lands in the right thread but reads as a brand-new message, with the original nowhere in it. To answer every participant use gog_gmail_reply_all. The gogcli-mcp-gmail package adds two more routes with the same composition: gog_gmail_autoreply to reply across every message matching a query, and gog_gmail_drafts_reply to stage this exact reply as a draft instead of sending it.' + CONFIRM_FALLBACK_DESCRIPTION,
37830
38502
  annotations: { destructiveHint: true },
37831
38503
  inputSchema: sendReplySchema
37832
- }, async ({ messageId, account, ...flags }, ctx) => {
38504
+ }, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
37833
38505
  assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
37834
38506
  confineReplyPaths(flags);
37835
- return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx);
38507
+ return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx, confirmToken);
37836
38508
  });
37837
38509
  server.registerTool("gog_gmail_reply_all", {
37838
- description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipients, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. To STAGE a reply-all instead of sending it, use gog_gmail_drafts_reply_all (gogcli-mcp-gmail only), which never needs confirmation. Reply to all participants of a Gmail message (the sender plus every To/Cc recipient). Same inherited "Re:" subject and quoted original as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all.',
38510
+ description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipients, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. To STAGE a reply-all instead of sending it, use gog_gmail_drafts_reply_all (gogcli-mcp-gmail only), which never needs confirmation. Reply to all participants of a Gmail message (the sender plus every To/Cc recipient). Same inherited "Re:" subject and quoted original as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all.' + CONFIRM_FALLBACK_DESCRIPTION,
37839
38511
  annotations: { destructiveHint: true },
37840
38512
  inputSchema: sendReplySchema
37841
- }, async ({ messageId, account, ...flags }, ctx) => {
38513
+ }, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
37842
38514
  assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
37843
38515
  confineReplyPaths(flags);
37844
- return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx);
38516
+ return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx, confirmToken);
37845
38517
  });
37846
38518
  registerRunTool(server, {
37847
38519
  service: "gmail",
@@ -38182,7 +38854,7 @@ function registerTasksTools(server) {
38182
38854
  }
38183
38855
 
38184
38856
  // src/server.ts
38185
- var VERSION = true ? "4.3.0" : "0.0.0";
38857
+ var VERSION = true ? "4.5.0" : "0.0.0";
38186
38858
  var BASE_TOOL_REGISTRARS = [
38187
38859
  registerApiTools,
38188
38860
  registerAppScriptTools,
@@ -38234,12 +38906,12 @@ async function bootstrapGogAuth(env = process.env, options = {}) {
38234
38906
  const [clientId, clientSecret, refreshToken, account] = values;
38235
38907
  const { spawner } = options;
38236
38908
  try {
38237
- const { createHash: createHash2 } = await import("node:crypto");
38909
+ const { createHash: createHash5 } = await import("node:crypto");
38238
38910
  const { mkdir, readFile, writeFile, chmod } = await import("node:fs/promises");
38239
38911
  const home = options.home ?? (await import("node:os")).homedir();
38240
38912
  const dir = join5(home, ".gogcli-mcp");
38241
38913
  const marker = join5(dir, AUTH_BOOTSTRAP_MARKER);
38242
- const fingerprint = createHash2("sha256").update(values.join("\0")).digest("hex");
38914
+ const fingerprint = createHash5("sha256").update(values.join("\0")).digest("hex");
38243
38915
  const previous = await readFile(marker, "utf8").catch(() => void 0);
38244
38916
  if (previous === fingerprint && await accountListed(account, spawner)) return "present";
38245
38917
  await run(