gogcli-mcp 4.2.5 → 4.4.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.
Files changed (63) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/index.js +1340 -262
  4. package/dist/lib.js +1381 -260
  5. package/manifest.json +9 -2
  6. package/package.json +2 -2
  7. package/server.json +2 -2
  8. package/src/arg-guard.ts +68 -0
  9. package/src/argv.ts +27 -0
  10. package/src/attachment-root.ts +111 -0
  11. package/src/attachments.ts +1 -0
  12. package/src/blob-upload.ts +4 -2
  13. package/src/dispatch-confirmation.ts +277 -0
  14. package/src/file-roots.ts +66 -0
  15. package/src/gmail-dispatch-guard.ts +70 -30
  16. package/src/gmail-results.ts +21 -2
  17. package/src/lib.ts +15 -0
  18. package/src/run-path-guard.ts +118 -0
  19. package/src/runner.ts +86 -18
  20. package/src/send-confirm-token.ts +167 -0
  21. package/src/tools/api.ts +60 -7
  22. package/src/tools/appscript.ts +12 -8
  23. package/src/tools/auth.ts +13 -3
  24. package/src/tools/calendar.ts +178 -15
  25. package/src/tools/chat.ts +94 -18
  26. package/src/tools/classroom.ts +110 -28
  27. package/src/tools/contacts.ts +5 -3
  28. package/src/tools/docs.ts +8 -6
  29. package/src/tools/drive.ts +96 -20
  30. package/src/tools/gmail.ts +175 -24
  31. package/src/tools/sheets.ts +10 -8
  32. package/src/tools/slides.ts +14 -9
  33. package/src/tools/tasks.ts +7 -5
  34. package/src/tools/utils.ts +52 -6
  35. package/tests/arg-guard.test.ts +80 -0
  36. package/tests/attachment-root.test.ts +130 -0
  37. package/tests/attachments.test.ts +8 -0
  38. package/tests/blob-upload.test.ts +4 -3
  39. package/tests/file-roots.test.ts +101 -0
  40. package/tests/gmail-dispatch-guard.test.ts +270 -11
  41. package/tests/gmail-results.test.ts +35 -1
  42. package/tests/run-path-guard.test.ts +142 -0
  43. package/tests/runner.test.ts +136 -0
  44. package/tests/send-confirm-token.test.ts +200 -0
  45. package/tests/tools/api.test.ts +74 -8
  46. package/tests/tools/appscript.test.ts +34 -8
  47. package/tests/tools/auth.test.ts +44 -17
  48. package/tests/tools/calendar.test.ts +33 -28
  49. package/tests/tools/chat.test.ts +50 -21
  50. package/tests/tools/classroom.test.ts +43 -39
  51. package/tests/tools/contacts.test.ts +3 -2
  52. package/tests/tools/dispatch-gates.test.ts +396 -0
  53. package/tests/tools/docs.test.ts +44 -15
  54. package/tests/tools/drive.test.ts +110 -20
  55. package/tests/tools/gmail-confirm-token.test.ts +274 -0
  56. package/tests/tools/gmail.test.ts +227 -29
  57. package/tests/tools/run-tool-examples.test.ts +69 -0
  58. package/tests/tools/run-vets.test.ts +131 -0
  59. package/tests/tools/sheets.test.ts +16 -15
  60. package/tests/tools/slides.test.ts +47 -11
  61. package/tests/tools/tasks.test.ts +7 -6
  62. package/tests/tools/utils.test.ts +32 -31
  63. package/vitest.config.ts +5 -0
package/dist/lib.js CHANGED
@@ -2001,8 +2001,8 @@ function emoji() {
2001
2001
  }
2002
2002
  var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
2003
2003
  var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
2004
- var mac = (delimiter2) => {
2005
- const escapedDelim = escapeRegex(delimiter2 ?? ":");
2004
+ var mac = (delimiter3) => {
2005
+ const escapedDelim = escapeRegex(delimiter3 ?? ":");
2006
2006
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
2007
2007
  };
2008
2008
  var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
@@ -5030,7 +5030,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
5030
5030
  var NONE = 0;
5031
5031
  var ASSUMED = 1;
5032
5032
  var PROVEN = 2;
5033
- function isRecursive(inst, stack, resolve) {
5033
+ function isRecursive(inst, stack, resolve4) {
5034
5034
  const cached2 = recursive.get(inst);
5035
5035
  if (cached2 !== void 0)
5036
5036
  return cached2 ? PROVEN : NONE;
@@ -5040,7 +5040,7 @@ function isRecursive(inst, stack, resolve) {
5040
5040
  let result = NONE;
5041
5041
  const check2 = (child) => {
5042
5042
  if (result !== PROVEN && child?._zod) {
5043
- const answer = isRecursive(child, stack, resolve);
5043
+ const answer = isRecursive(child, stack, resolve4);
5044
5044
  if (answer > result)
5045
5045
  result = answer;
5046
5046
  }
@@ -5051,7 +5051,7 @@ function isRecursive(inst, stack, resolve) {
5051
5051
  const desc = Object.getOwnPropertyDescriptor(sh, key);
5052
5052
  if (spread && !desc.enumerable)
5053
5053
  continue;
5054
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve) : NONE;
5054
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve4) : NONE;
5055
5055
  if (child > answer)
5056
5056
  answer = child;
5057
5057
  }
@@ -5115,7 +5115,7 @@ function isRecursive(inst, stack, resolve) {
5115
5115
  break;
5116
5116
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5117
5117
  case "lazy": {
5118
- const inner = def._cachedInner ?? (resolve ? inst._zod.innerType : void 0);
5118
+ const inner = def._cachedInner ?? (resolve4 ? inst._zod.innerType : void 0);
5119
5119
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
5120
5120
  break;
5121
5121
  }
@@ -23998,14 +23998,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
23998
23998
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
23999
23999
  }
24000
24000
  function sleep(ms, signal) {
24001
- return new Promise((resolve, reject) => {
24001
+ return new Promise((resolve4, reject) => {
24002
24002
  if (signal?.aborted) {
24003
24003
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
24004
24004
  return;
24005
24005
  }
24006
24006
  const timer = setTimeout(() => {
24007
24007
  signal?.removeEventListener("abort", onAbort);
24008
- resolve();
24008
+ resolve4();
24009
24009
  }, ms);
24010
24010
  const onAbort = () => {
24011
24011
  clearTimeout(timer);
@@ -24796,7 +24796,7 @@ var Protocol = class {
24796
24796
  const flowStartedAt = Date.now();
24797
24797
  let onAbort;
24798
24798
  let cleanupMessageId;
24799
- return new Promise((resolve, reject) => {
24799
+ return new Promise((resolve4, reject) => {
24800
24800
  const earlyReject = (error62) => {
24801
24801
  reject(error62);
24802
24802
  };
@@ -24864,7 +24864,7 @@ var Protocol = class {
24864
24864
  }
24865
24865
  if (decoded.kind === "invalid") return reject(decoded.error);
24866
24866
  if (decoded.kind === "input_required") {
24867
- if (options?.allowInputRequired === true) return resolve(manualInputRequiredValue(decoded));
24867
+ if (options?.allowInputRequired === true) return resolve4(manualInputRequiredValue(decoded));
24868
24868
  const flow = {
24869
24869
  codec: codec2,
24870
24870
  request,
@@ -24876,11 +24876,11 @@ var Protocol = class {
24876
24876
  params
24877
24877
  }, resultSchema, legOptions)
24878
24878
  };
24879
- return resolve(this._resolveNonCompleteResult(decoded, flow));
24879
+ return resolve4(this._resolveNonCompleteResult(decoded, flow));
24880
24880
  }
24881
24881
  const result = decoded.result;
24882
24882
  validateStandardSchema(resultSchema, result).then((parseResult) => {
24883
- if (parseResult.success) resolve(parseResult.data);
24883
+ if (parseResult.success) resolve4(parseResult.data);
24884
24884
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
24885
24885
  }, reject);
24886
24886
  });
@@ -27692,7 +27692,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
27692
27692
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
27693
27693
  const schOrFunc = root.refs[ref];
27694
27694
  if (schOrFunc) return schOrFunc;
27695
- let _sch = resolve.call(this, root, ref);
27695
+ let _sch = resolve4.call(this, root, ref);
27696
27696
  if (_sch === void 0) {
27697
27697
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
27698
27698
  const { schemaId } = this.opts;
@@ -27718,7 +27718,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
27718
27718
  function sameSchemaEnv(s1, s2) {
27719
27719
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
27720
27720
  }
27721
- function resolve(root, ref) {
27721
+ function resolve4(root, ref) {
27722
27722
  let sch;
27723
27723
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
27724
27724
  return sch || this.schemas[ref] || resolveSchema.call(this, root, ref);
@@ -28168,47 +28168,47 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
28168
28168
  else if (typeof uri === "object") uri = parse3(serialize(uri, options), options);
28169
28169
  return uri;
28170
28170
  }
28171
- function resolve(baseURI, relativeURI, options) {
28171
+ function resolve4(baseURI, relativeURI, options) {
28172
28172
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
28173
28173
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
28174
28174
  schemelessOptions.skipEscape = true;
28175
28175
  return serialize(resolved, schemelessOptions);
28176
28176
  }
28177
- function resolveComponent(base, relative, options, skipNormalization) {
28177
+ function resolveComponent(base, relative3, options, skipNormalization) {
28178
28178
  const target = {};
28179
28179
  if (!skipNormalization) {
28180
28180
  base = parse3(serialize(base, options), options);
28181
- relative = parse3(serialize(relative, options), options);
28181
+ relative3 = parse3(serialize(relative3, options), options);
28182
28182
  }
28183
28183
  options = options || {};
28184
- if (!options.tolerant && relative.scheme) {
28185
- target.scheme = relative.scheme;
28186
- target.userinfo = relative.userinfo;
28187
- target.host = relative.host;
28188
- target.port = relative.port;
28189
- target.path = removeDotSegments(relative.path || "");
28190
- target.query = relative.query;
28184
+ if (!options.tolerant && relative3.scheme) {
28185
+ target.scheme = relative3.scheme;
28186
+ target.userinfo = relative3.userinfo;
28187
+ target.host = relative3.host;
28188
+ target.port = relative3.port;
28189
+ target.path = removeDotSegments(relative3.path || "");
28190
+ target.query = relative3.query;
28191
28191
  } else {
28192
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
28193
- target.userinfo = relative.userinfo;
28194
- target.host = relative.host;
28195
- target.port = relative.port;
28196
- target.path = removeDotSegments(relative.path || "");
28197
- target.query = relative.query;
28192
+ if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
28193
+ target.userinfo = relative3.userinfo;
28194
+ target.host = relative3.host;
28195
+ target.port = relative3.port;
28196
+ target.path = removeDotSegments(relative3.path || "");
28197
+ target.query = relative3.query;
28198
28198
  } else {
28199
- if (!relative.path) {
28199
+ if (!relative3.path) {
28200
28200
  target.path = base.path;
28201
- if (relative.query !== void 0) target.query = relative.query;
28201
+ if (relative3.query !== void 0) target.query = relative3.query;
28202
28202
  else target.query = base.query;
28203
28203
  } else {
28204
- if (relative.path[0] === "/") target.path = removeDotSegments(relative.path);
28204
+ if (relative3.path[0] === "/") target.path = removeDotSegments(relative3.path);
28205
28205
  else {
28206
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative.path;
28207
- else if (!base.path) target.path = relative.path;
28208
- else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
28206
+ if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative3.path;
28207
+ else if (!base.path) target.path = relative3.path;
28208
+ else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
28209
28209
  target.path = removeDotSegments(target.path);
28210
28210
  }
28211
- target.query = relative.query;
28211
+ target.query = relative3.query;
28212
28212
  }
28213
28213
  target.userinfo = base.userinfo;
28214
28214
  target.host = base.host;
@@ -28216,7 +28216,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
28216
28216
  }
28217
28217
  target.scheme = base.scheme;
28218
28218
  }
28219
- target.fragment = relative.fragment;
28219
+ target.fragment = relative3.fragment;
28220
28220
  return target;
28221
28221
  }
28222
28222
  function equal(uriA, uriB, options) {
@@ -28342,7 +28342,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
28342
28342
  const fastUri = {
28343
28343
  SCHEMES,
28344
28344
  normalize,
28345
- resolve,
28345
+ resolve: resolve4,
28346
28346
  resolveComponent,
28347
28347
  equal,
28348
28348
  serialize,
@@ -29124,14 +29124,14 @@ var require_ucs2length = /* @__PURE__ */ __commonJSMin(((exports) => {
29124
29124
  function ucs2length(str) {
29125
29125
  const len = str.length;
29126
29126
  let length = 0;
29127
- let pos = 0;
29127
+ let pos2 = 0;
29128
29128
  let value;
29129
- while (pos < len) {
29129
+ while (pos2 < len) {
29130
29130
  length++;
29131
- value = str.charCodeAt(pos++);
29132
- if (value >= 55296 && value <= 56319 && pos < len) {
29133
- value = str.charCodeAt(pos);
29134
- if ((value & 64512) === 56320) pos++;
29131
+ value = str.charCodeAt(pos2++);
29132
+ if (value >= 55296 && value <= 56319 && pos2 < len) {
29133
+ value = str.charCodeAt(pos2);
29134
+ if ((value & 64512) === 56320) pos2++;
29135
29135
  }
29136
29136
  }
29137
29137
  return length;
@@ -32900,13 +32900,28 @@ var API_KEY_RE = new RegExp([
32900
32900
  "whsec_[A-Za-z0-9]{16,}\\b"
32901
32901
  // webhook signing secret (Stripe-style)
32902
32902
  ].map((p) => `\\b${p}`).join("|"), "g");
32903
- var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
32903
+ var QUERY_SECRET_RE = /([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
32904
+ var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
32905
+ var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
32906
+ var HEADER_SECRET_RE = /(\b(?:x[-_][a-z0-9_-]*?(?:api[-_]?key|token|secret|auth)[a-z0-9_-]*|api[-_]?key)\s*:\s*)[^\s,;"'<>`]+/gi;
32904
32907
  var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
32905
- var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
32906
- var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
32907
- var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
32908
+ var JSON_SECRET_KEYS = [
32909
+ "(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
32910
+ "client[_-]?secret",
32911
+ "api[_-]?secret",
32912
+ "(?:x[_-])?api[_-]?key",
32913
+ "private[_-]?key",
32914
+ "x[_-][a-z0-9_-]*?(?:api[_-]?key|token|secret|auth)[a-z0-9_-]*",
32915
+ "(?:proxy-)?authorization",
32916
+ "password",
32917
+ "passwd",
32918
+ "secret",
32919
+ "token"
32920
+ ].join("|");
32921
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
32922
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')(?:[^'\\\\]|\\\\.)*(')`, "gi");
32908
32923
  function redactSecrets(text) {
32909
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
32924
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(HEADER_SECRET_RE, "$1[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(OAUTH_CODE_RE, "$1[REDACTED]").replace(FORM_PASSWORD_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
32910
32925
  }
32911
32926
 
32912
32927
  // ../../node_modules/@chrischall/mcp-utils/dist/response/view.js
@@ -33001,26 +33016,105 @@ function errorResult(message) {
33001
33016
  }
33002
33017
 
33003
33018
  // ../../node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
33019
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
33004
33020
  var DEFAULT_REQUEST_KEY = "confirmation";
33005
33021
  var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
33006
33022
  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";
33023
+ var STATE_PREFIX = "mcpu.confirm.v1.";
33024
+ function canonicalJson(value) {
33025
+ if (value === null || typeof value === "string" || typeof value === "boolean")
33026
+ return JSON.stringify(value);
33027
+ if (value === void 0)
33028
+ return "null";
33029
+ if (typeof value === "number")
33030
+ return Number.isFinite(value) ? JSON.stringify(value) : `{"$num":"${String(value)}"}`;
33031
+ if (typeof value === "bigint")
33032
+ return `{"$bigint":"${value.toString()}"}`;
33033
+ if (typeof value === "function" || typeof value === "symbol") {
33034
+ throw new TypeError(`requireConfirmation: cannot bind a ${typeof value} argument value.`);
33035
+ }
33036
+ if (Array.isArray(value))
33037
+ return `[${value.map(canonicalJson).join(",")}]`;
33038
+ if (value instanceof Date) {
33039
+ return `{"$date":${JSON.stringify(Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString())}}`;
33040
+ }
33041
+ if (ArrayBuffer.isView(value)) {
33042
+ const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
33043
+ return `{"$bytes":${JSON.stringify(bytes.toString("base64"))}}`;
33044
+ }
33045
+ if (value instanceof ArrayBuffer)
33046
+ return `{"$bytes":${JSON.stringify(Buffer.from(value).toString("base64"))}}`;
33047
+ if (value instanceof Map) {
33048
+ const entries2 = [...value.entries()].map(([k, v]) => `[${canonicalJson(k)},${canonicalJson(v)}]`).sort();
33049
+ return `{"$map":[${entries2.join(",")}]}`;
33050
+ }
33051
+ if (value instanceof Set) {
33052
+ return `{"$set":[${[...value].map(canonicalJson).sort().join(",")}]}`;
33053
+ }
33054
+ const proto = Object.getPrototypeOf(value);
33055
+ if (proto !== Object.prototype && proto !== null) {
33056
+ throw new TypeError("requireConfirmation: cannot bind a non-plain object argument value (class instance).");
33057
+ }
33058
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
33059
+ return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
33060
+ }
33061
+ function escapeKey(key) {
33062
+ return key.startsWith("$") ? `$${key}` : key;
33063
+ }
33064
+ function bindingKey(binding) {
33065
+ if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
33066
+ throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
33067
+ }
33068
+ const key = typeof binding.key === "string" ? Buffer.from(binding.key, "utf8") : Buffer.from(binding.key);
33069
+ if (key.length < 32) {
33070
+ throw new RangeError("requireConfirmation: binding.key must be at least 32 bytes.");
33071
+ }
33072
+ return key;
33073
+ }
33074
+ function commitment(action, args) {
33075
+ return createHash("sha256").update(`${action}\0${canonicalJson(args)}`).digest("base64url");
33076
+ }
33077
+ function mintState(key, action, binding) {
33078
+ const exp = Math.floor(Date.now() / 1e3) + (binding.ttlSeconds ?? 600);
33079
+ const body = Buffer.from(JSON.stringify({ c: commitment(action, binding.args), exp })).toString("base64url");
33080
+ const mac3 = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest("base64url");
33081
+ return `${STATE_PREFIX}${body}.${mac3}`;
33082
+ }
33083
+ function verifyState(key, action, binding, state) {
33084
+ if (typeof state !== "string" || !state.startsWith(STATE_PREFIX))
33085
+ return false;
33086
+ const rest = state.slice(STATE_PREFIX.length);
33087
+ const dot = rest.indexOf(".");
33088
+ if (dot < 0)
33089
+ return false;
33090
+ const body = rest.slice(0, dot);
33091
+ const mac3 = Buffer.from(rest.slice(dot + 1), "base64url");
33092
+ const expected = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest();
33093
+ if (mac3.length !== expected.length || !timingSafeEqual(mac3, expected))
33094
+ return false;
33095
+ let payload;
33096
+ try {
33097
+ payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
33098
+ } catch {
33099
+ return false;
33100
+ }
33101
+ if (typeof payload.exp !== "number" || payload.exp * 1e3 <= Date.now())
33102
+ return false;
33103
+ const want = Buffer.from(commitment(action, binding.args));
33104
+ const got = Buffer.from(typeof payload.c === "string" ? payload.c : "");
33105
+ return got.length === want.length && timingSafeEqual(got, want);
33106
+ }
33107
+ function echoedState(ctx) {
33108
+ const accessor = ctx.mcpReq.requestState;
33109
+ return typeof accessor === "function" ? accessor() : void 0;
33110
+ }
33007
33111
  function requireConfirmation(ctx, options) {
33008
33112
  const requestKey = options.requestKey ?? DEFAULT_REQUEST_KEY;
33113
+ const key = options.binding ? bindingKey(options.binding) : void 0;
33009
33114
  const confirmationSchema = external_exports.object({
33010
33115
  confirmed: external_exports.boolean().describe(options.confirmationLabel ?? DEFAULT_CONFIRMATION_LABEL)
33011
33116
  });
33012
- const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
33013
- const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
33014
- if (response.kind === "missing") {
33015
- if (callerAcceptsFormElicitation(ctx) === false) {
33016
- return textResult({
33017
- confirmed: false,
33018
- dispatched: false,
33019
- action: options.action,
33020
- reason: "confirmation-unsupported",
33021
- note: options.unsupportedNote ? `${UNSUPPORTED_NOTE}. ${options.unsupportedNote}` : `${UNSUPPORTED_NOTE}.`
33022
- });
33023
- }
33117
+ const ask = () => {
33024
33118
  const preview = {
33025
33119
  action: options.action,
33026
33120
  ...options.details === void 0 ? {} : { details: options.details }
@@ -33032,10 +33126,33 @@ function requireConfirmation(ctx, options) {
33032
33126
  ${JSON.stringify(preview, null, 2)}`,
33033
33127
  requestedSchema: confirmationSchema
33034
33128
  })
33035
- }
33129
+ },
33130
+ ...key && options.binding ? { requestState: mintState(key, options.action, options.binding) } : {}
33036
33131
  });
33132
+ };
33133
+ const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
33134
+ const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
33135
+ if (response.kind === "missing") {
33136
+ if (callerAcceptsFormElicitation(ctx) === false) {
33137
+ return textResult({
33138
+ confirmed: false,
33139
+ dispatched: false,
33140
+ action: options.action,
33141
+ reason: "confirmation-unsupported",
33142
+ note: options.unsupportedNote ? `${UNSUPPORTED_NOTE}. ${options.unsupportedNote}` : `${UNSUPPORTED_NOTE}.`
33143
+ });
33144
+ }
33145
+ return ask();
33037
33146
  }
33038
33147
  if (response.kind === "elicit" && response.action === "accept" && accepted?.confirmed === true) {
33148
+ if (key && options.binding) {
33149
+ const state = echoedState(ctx);
33150
+ if (state === void 0 || state === null) {
33151
+ 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.`);
33152
+ }
33153
+ if (!verifyState(key, options.action, options.binding, state))
33154
+ return ask();
33155
+ }
33039
33156
  return void 0;
33040
33157
  }
33041
33158
  return textResult({
@@ -33053,6 +33170,8 @@ var SERVER_PROTOCOL_VERSIONS = Object.freeze([
33053
33170
  ]);
33054
33171
 
33055
33172
  // ../../node_modules/@chrischall/mcp-utils/dist/config/index.js
33173
+ import { homedir } from "node:os";
33174
+ import { isAbsolute, join, resolve } from "node:path";
33056
33175
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
33057
33176
  function readEnvVar(key, opts = {}) {
33058
33177
  const env = opts.env ?? process.env;
@@ -33079,6 +33198,50 @@ function parseBoolEnv(key, opts = {}) {
33079
33198
  return false;
33080
33199
  return fallback;
33081
33200
  }
33201
+ function expandPath(p) {
33202
+ let expanded = p;
33203
+ if (p === "~") {
33204
+ expanded = homedir();
33205
+ } else if (p.startsWith("~/")) {
33206
+ expanded = join(homedir(), p.slice(2));
33207
+ }
33208
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
33209
+ }
33210
+
33211
+ // ../../node_modules/@chrischall/mcp-utils/dist/fs/confine.js
33212
+ import { existsSync, realpathSync } from "node:fs";
33213
+ import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
33214
+ function isInside(root, target) {
33215
+ const rel = relative(root, target);
33216
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute2(rel);
33217
+ }
33218
+ function realPathAllowingMissing(p) {
33219
+ const abs = resolve2(p);
33220
+ let existing = abs;
33221
+ const tail = [];
33222
+ while (!existsSync(existing)) {
33223
+ const parent = dirname(existing);
33224
+ if (parent === existing)
33225
+ break;
33226
+ tail.unshift(existing.slice(parent.length).replace(/^[/\\]+/, ""));
33227
+ existing = parent;
33228
+ }
33229
+ let real;
33230
+ try {
33231
+ real = realpathSync(existing);
33232
+ } catch {
33233
+ real = existing;
33234
+ }
33235
+ return tail.length > 0 ? resolve2(real, ...tail) : real;
33236
+ }
33237
+ function assertPathWithinRoots(path, roots) {
33238
+ const target = realPathAllowingMissing(expandPath(path));
33239
+ for (const root of roots) {
33240
+ if (isInside(realPathAllowingMissing(expandPath(root)), target))
33241
+ return target;
33242
+ }
33243
+ throw new Error(`Path is outside the allowed directories: ${path}`);
33244
+ }
33082
33245
 
33083
33246
  // ../../node_modules/@chrischall/mcp-utils/dist/zod/index.js
33084
33247
  var PositiveInt = external_exports.number().int().positive();
@@ -33100,7 +33263,7 @@ var pageSchema = {
33100
33263
  };
33101
33264
 
33102
33265
  // src/runner.ts
33103
- import { delimiter, join } from "node:path";
33266
+ import { delimiter, join as join2 } from "node:path";
33104
33267
 
33105
33268
  // src/timestamps.ts
33106
33269
  var DEFAULT_DISPLAY_TZ = "America/New_York";
@@ -33126,9 +33289,9 @@ function offsetAt(instant, tz) {
33126
33289
  const w = wallPartsIn(instant, tz);
33127
33290
  const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
33128
33291
  const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
33129
- const sign2 = minutes < 0 ? "-" : "+";
33292
+ const sign3 = minutes < 0 ? "-" : "+";
33130
33293
  const abs = Math.abs(minutes);
33131
- return `${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
33294
+ return `${sign3}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
33132
33295
  }
33133
33296
  function wallPartsIn(instant, tz) {
33134
33297
  const parts = new Intl.DateTimeFormat("en-US", {
@@ -33301,9 +33464,48 @@ function normalizeTimestamps(text, tz = displayTimeZone(), naiveTz = naiveSource
33301
33464
  return JSON.stringify(parsed, null, detectIndent(text));
33302
33465
  }
33303
33466
 
33467
+ // src/arg-guard.ts
33468
+ var FORBIDDEN_LONG_FLAG = /^--(readonly|enable-commands|enable-commands-exact|disable-commands|access-token|home|account|acct|client|gmail-no-send|no-input|non-interactive|noninteractive)(=|$)/i;
33469
+ var SHORT_ACCOUNT_CLUSTER = /^-(?!-)[A-Za-z]*a/;
33470
+ function forbiddenArgReason(arg) {
33471
+ if (arg === "--") {
33472
+ return 'The argument "--" is not allowed: it ends flag parsing and would disable safety flags appended after it.';
33473
+ }
33474
+ const match = FORBIDDEN_LONG_FLAG.exec(arg);
33475
+ if (match) {
33476
+ return `The flag ${arg} is not allowed here: --${match[1].toLowerCase()} is a safety/credential control set by the server operator, not a per-call option.`;
33477
+ }
33478
+ if (SHORT_ACCOUNT_CLUSTER.test(arg)) {
33479
+ return `The flag ${arg} is not allowed here: -a selects the account. Use the tool's account parameter instead.`;
33480
+ }
33481
+ return void 0;
33482
+ }
33483
+ function assertSafeForwardedArgs(args) {
33484
+ for (const arg of args) {
33485
+ const reason = forbiddenArgReason(arg);
33486
+ if (reason) throw new Error(reason);
33487
+ }
33488
+ }
33489
+ var SUBCOMMAND_SHAPE = /^[a-z][a-z0-9-]*$/;
33490
+ function assertSafeSubcommand(subcommand) {
33491
+ if (!SUBCOMMAND_SHAPE.test(subcommand)) {
33492
+ throw new Error(
33493
+ `Invalid subcommand ${JSON.stringify(subcommand)}: pass a single gog subcommand name such as "list" or "mark-read", and put its arguments in args.`
33494
+ );
33495
+ }
33496
+ }
33497
+
33498
+ // src/argv.ts
33499
+ function pos(value) {
33500
+ return { kind: "positional", value };
33501
+ }
33502
+ function isGogPositional(arg) {
33503
+ return typeof arg === "object" && arg !== null && arg.kind === "positional";
33504
+ }
33505
+
33304
33506
  // src/runner.ts
33305
33507
  function isGogFileArg(arg) {
33306
- return typeof arg !== "string";
33508
+ return typeof arg !== "string" && arg.kind === "file";
33307
33509
  }
33308
33510
  var TIMEOUT_MS = 3e4;
33309
33511
  var MIN_GOG_VERSION = "0.41.0";
@@ -33388,8 +33590,8 @@ function formatTimeout(ms) {
33388
33590
  }
33389
33591
  async function spawnWithTempFiles(args, opts) {
33390
33592
  const { mkdtemp, mkdir, writeFile, rm } = await import("node:fs/promises");
33391
- const { tmpdir } = await import("node:os");
33392
- const dir = await mkdtemp(join(tmpdir(), "gogcli-mcp-"));
33593
+ const { tmpdir: tmpdir2 } = await import("node:os");
33594
+ const dir = await mkdtemp(join2(tmpdir2(), "gogcli-mcp-"));
33393
33595
  try {
33394
33596
  const argv = [];
33395
33597
  let seq = 0;
@@ -33398,10 +33600,10 @@ async function spawnWithTempFiles(args, opts) {
33398
33600
  argv.push(arg);
33399
33601
  continue;
33400
33602
  }
33401
- const sub = join(dir, String(seq));
33603
+ const sub = join2(dir, String(seq));
33402
33604
  seq += 1;
33403
33605
  await mkdir(sub, { recursive: true, mode: 448 });
33404
- const path = join(sub, arg.filename ?? `${arg.flag}.${arg.ext ?? "txt"}`);
33606
+ const path = join2(sub, arg.filename ?? `${arg.flag}.${arg.ext ?? "txt"}`);
33405
33607
  const data = arg.encoding === "base64" ? Buffer.from(arg.contents, "base64") : Buffer.from(arg.contents, "utf8");
33406
33608
  await writeFile(path, data, { mode: 384 });
33407
33609
  argv.push(arg.positional ? path : `--${arg.flag}=${path}`);
@@ -33419,10 +33621,10 @@ function spawnExecutor(args, opts) {
33419
33621
  return spawnGog(args, opts);
33420
33622
  }
33421
33623
  async function spawnGog(fullArgs, opts) {
33422
- const { timeout, interactive = false, spawner, binary = false } = opts;
33624
+ const { timeout, interactive = false, spawner, binary = false, maxOutputBytes } = opts;
33423
33625
  const spawn = spawner ?? (await import("node:child_process")).spawn;
33424
33626
  const effectiveTimeout = timeout ?? TIMEOUT_MS;
33425
- return new Promise((resolve, reject) => {
33627
+ return new Promise((resolve4, reject) => {
33426
33628
  const childEnv = { ...sanitizedEnv(), GOG_TIMEZONE: naiveSourceTimeZone(), PATH: augmentedPath() };
33427
33629
  const child = spawn(readEnvVar("GOG_PATH") ?? "gog", fullArgs, { env: childEnv });
33428
33630
  const stdoutChunks = [];
@@ -33440,7 +33642,17 @@ async function spawnGog(fullArgs, opts) {
33440
33642
  child.kill();
33441
33643
  reject(new Error(`gog timed out after ${formatTimeout(effectiveTimeout)}`));
33442
33644
  }, effectiveTimeout);
33645
+ let stdoutBytes = 0;
33443
33646
  child.stdout.on("data", (chunk) => {
33647
+ if (settled) return;
33648
+ stdoutBytes += chunk.length;
33649
+ if (maxOutputBytes !== void 0 && stdoutBytes > maxOutputBytes) {
33650
+ settled = true;
33651
+ stopWatching();
33652
+ child.kill();
33653
+ reject(new Error(`gog output exceeded the limit: more than ${maxOutputBytes} bytes`));
33654
+ return;
33655
+ }
33444
33656
  stdoutChunks.push(chunk);
33445
33657
  });
33446
33658
  child.stderr.on("data", (chunk) => {
@@ -33457,14 +33669,14 @@ async function spawnGog(fullArgs, opts) {
33457
33669
  const stderr = Buffer.concat(stderrChunks).toString().trim();
33458
33670
  if (code === 0) {
33459
33671
  if (binary) {
33460
- resolve(Buffer.concat(stdoutChunks).toString("base64"));
33672
+ resolve4(Buffer.concat(stdoutChunks).toString("base64"));
33461
33673
  return;
33462
33674
  }
33463
33675
  const stdout = Buffer.concat(stdoutChunks).toString();
33464
33676
  if (interactive && stderr) {
33465
- resolve(stdout + "\n" + stderr);
33677
+ resolve4(stdout + "\n" + stderr);
33466
33678
  } else {
33467
- resolve(stdout);
33679
+ resolve4(stdout);
33468
33680
  }
33469
33681
  } else {
33470
33682
  reject(new Error(stderr || `gog exited with code ${code}`));
@@ -33484,7 +33696,33 @@ async function spawnGog(fullArgs, opts) {
33484
33696
  });
33485
33697
  });
33486
33698
  }
33699
+ function placePositionals(args) {
33700
+ const flags = [];
33701
+ const positionals = [];
33702
+ let afterSeparator = false;
33703
+ for (const arg of args) {
33704
+ if (arg === "--") {
33705
+ afterSeparator = true;
33706
+ } else if (isGogPositional(arg)) {
33707
+ positionals.push(arg.value);
33708
+ } else if (afterSeparator || isGogFileArg(arg) && arg.positional) {
33709
+ positionals.push(arg);
33710
+ } else {
33711
+ flags.push(arg);
33712
+ }
33713
+ }
33714
+ return { flags, positionals };
33715
+ }
33716
+ function assertNoSafetyOverrides(flags) {
33717
+ for (const arg of flags) {
33718
+ if (typeof arg !== "string") continue;
33719
+ const reason = forbiddenArgReason(arg);
33720
+ if (reason) throw new Error(reason);
33721
+ }
33722
+ }
33487
33723
  function assembleArgs(args, opts) {
33724
+ const { flags, positionals } = placePositionals(args);
33725
+ assertNoSafetyOverrides(flags);
33488
33726
  const effectiveAccount = opts.account ?? readEnvVar("GOG_ACCOUNT");
33489
33727
  const fullArgs = ["--json", "--color=never"];
33490
33728
  if (!opts.interactive) {
@@ -33493,17 +33731,21 @@ function assembleArgs(args, opts) {
33493
33731
  if (opts.readonly || readonlyEnvEnabled()) {
33494
33732
  fullArgs.push("--readonly");
33495
33733
  }
33734
+ if (opts.gmailNoSend) {
33735
+ fullArgs.push("--gmail-no-send");
33736
+ }
33496
33737
  if (effectiveAccount) {
33497
33738
  fullArgs.push("--account", effectiveAccount);
33498
33739
  }
33499
- fullArgs.push(...args);
33740
+ fullArgs.push(...flags);
33741
+ if (positionals.length > 0) fullArgs.push("--", ...positionals);
33500
33742
  return fullArgs;
33501
33743
  }
33502
33744
  async function run(args, options = {}) {
33503
- const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full", opaqueFields } = options;
33745
+ const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full", opaqueFields, gmailNoSend = false } = options;
33504
33746
  const base = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
33505
33747
  const redact = opaqueFields?.length ? (text) => redactPreservingOpaqueFields(text, opaqueFields, base) : base;
33506
- const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
33748
+ const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2, gmailNoSend });
33507
33749
  try {
33508
33750
  return redact(await spawnExecutor(fullArgs, { timeout, interactive, spawner }));
33509
33751
  } catch (err) {
@@ -33511,9 +33753,9 @@ async function run(args, options = {}) {
33511
33753
  }
33512
33754
  }
33513
33755
  async function runBinary(args, options = {}) {
33514
- const { account, spawner, timeout, readonly: readonly2 = false } = options;
33515
- const fullArgs = assembleArgs(args, { account, interactive: false, readonly: readonly2 });
33516
- return spawnExecutor(fullArgs, { timeout, interactive: false, spawner, binary: true });
33756
+ const { account, spawner, timeout, readonly: readonly2 = false, gmailNoSend = false, maxOutputBytes } = options;
33757
+ const fullArgs = assembleArgs(args, { account, interactive: false, readonly: readonly2, gmailNoSend });
33758
+ return spawnExecutor(fullArgs, { timeout, interactive: false, spawner, binary: true, maxOutputBytes });
33517
33759
  }
33518
33760
 
33519
33761
  // src/pagination.ts
@@ -33565,6 +33807,175 @@ function annotateTruncatedList(result, itemsKey) {
33565
33807
  return rawTextResult(JSON.stringify(out));
33566
33808
  }
33567
33809
 
33810
+ // src/file-roots.ts
33811
+ import { homedir as homedir2 } from "node:os";
33812
+ import { delimiter as delimiter2, join as join4 } from "node:path";
33813
+
33814
+ // src/attachment-root.ts
33815
+ import { tmpdir, userInfo } from "node:os";
33816
+ import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
33817
+ var ATTACHMENT_DOWNLOAD_ROOT = join3(tmpdir(), `gogcli-mcp-attachments-${userInfo().uid}`);
33818
+ var ATTACHMENT_TTL_MS = 24 * 60 * 60 * 1e3;
33819
+ function isInside2(root, target) {
33820
+ const rel = relative2(root, target);
33821
+ return rel !== "" && !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute3(rel);
33822
+ }
33823
+ async function ensurePrivateDir(dir, opts = {}) {
33824
+ const { mkdir, lstat, chmod } = await import("node:fs/promises");
33825
+ try {
33826
+ await mkdir(dir, { mode: 448 });
33827
+ } catch (err) {
33828
+ if (err.code !== "EEXIST") throw err;
33829
+ }
33830
+ const info = await lstat(dir);
33831
+ if (info.isSymbolicLink() || !info.isDirectory()) {
33832
+ throw new Error(`refusing to use ${dir} for attachment downloads: it is not a real directory`);
33833
+ }
33834
+ const uid = opts.uid ?? userInfo().uid;
33835
+ if (info.uid !== uid) {
33836
+ throw new Error(`refusing to use ${dir} for attachment downloads: it is owned by another user`);
33837
+ }
33838
+ if ((info.mode & 63) !== 0) await chmod(dir, 448);
33839
+ }
33840
+ async function sweepExpiredDownloads(root = ATTACHMENT_DOWNLOAD_ROOT, ttlMs = ATTACHMENT_TTL_MS, now = Date.now()) {
33841
+ const { readdir, lstat, rm } = await import("node:fs/promises");
33842
+ let entries;
33843
+ try {
33844
+ entries = await readdir(root);
33845
+ } catch {
33846
+ return 0;
33847
+ }
33848
+ let removed = 0;
33849
+ for (const name of entries) {
33850
+ const path = join3(root, name);
33851
+ try {
33852
+ const info = await lstat(path);
33853
+ if (now - info.mtimeMs > ttlMs) {
33854
+ await rm(path, { recursive: true, force: true });
33855
+ removed += 1;
33856
+ }
33857
+ } catch {
33858
+ }
33859
+ }
33860
+ return removed;
33861
+ }
33862
+ async function removeDownload(path, root = ATTACHMENT_DOWNLOAD_ROOT) {
33863
+ const { rm, rmdir } = await import("node:fs/promises");
33864
+ const base = resolve3(root);
33865
+ const target = resolve3(base, path);
33866
+ if (!isInside2(base, target)) return;
33867
+ await rm(target, { force: true });
33868
+ for (let dir = dirname2(target); isInside2(base, dir); dir = dirname2(dir)) {
33869
+ try {
33870
+ await rmdir(dir);
33871
+ } catch {
33872
+ break;
33873
+ }
33874
+ }
33875
+ }
33876
+ async function prepareDownloadRoot(root = ATTACHMENT_DOWNLOAD_ROOT) {
33877
+ await ensurePrivateDir(root);
33878
+ await sweepExpiredDownloads(root);
33879
+ }
33880
+
33881
+ // src/file-roots.ts
33882
+ var FILE_ROOTS_ENV = "GOG_FILE_ROOTS";
33883
+ function defaultFileRoot() {
33884
+ return join4(homedir2(), "gogcli-mcp-files");
33885
+ }
33886
+ function fileRoots() {
33887
+ const raw = readEnvVar(FILE_ROOTS_ENV);
33888
+ const roots = (raw ?? "").split(delimiter2).map((r) => r.trim()).filter(Boolean);
33889
+ return roots.length > 0 ? roots : [defaultFileRoot()];
33890
+ }
33891
+ function confinePath(path, param, opts = {}) {
33892
+ if (opts.allowDash && path === "-") return path;
33893
+ const roots = [...fileRoots(), ATTACHMENT_DOWNLOAD_ROOT];
33894
+ try {
33895
+ assertPathWithinRoots(path, roots);
33896
+ } catch {
33897
+ throw new Error(
33898
+ `${param} ${JSON.stringify(path)} is outside the directories this server may read or write (${roots.join(", ")}). Put the file in one of them, or ask the server operator to widen ${FILE_ROOTS_ENV}.`
33899
+ );
33900
+ }
33901
+ return path;
33902
+ }
33903
+ function confinePaths(paths, param) {
33904
+ for (const path of paths ?? []) confinePath(path, param);
33905
+ }
33906
+ function confineAtFile(value, param) {
33907
+ if (value.startsWith("@")) confinePath(value.slice(1), param);
33908
+ return value;
33909
+ }
33910
+
33911
+ // src/run-path-guard.ts
33912
+ var POSITIONAL_PATH_SUBCOMMANDS = {
33913
+ appscript: { pull: "gog_appscript_pull" },
33914
+ drive: { upload: "gog_drive_upload", sync: "gog_drive_sync_push" },
33915
+ gmail: { import: "gog_gmail_import" },
33916
+ slides: {
33917
+ "add-slide": "gog_slides_add_slide",
33918
+ "insert-image": "gog_slides_insert_image",
33919
+ "replace-slide": "gog_slides_replace_slide"
33920
+ }
33921
+ };
33922
+ var PATH_FLAGS = /* @__PURE__ */ new Set(["out", "output", "out-dir", "output-dir", "dir", "worker-dir", "attach", "file", "key", "cert", "replacements"]);
33923
+ var ID_FLAGS = {
33924
+ drive: /* @__PURE__ */ new Set(["file", "filter-file"])
33925
+ };
33926
+ var EXEC_FLAGS = /* @__PURE__ */ new Set(["on-change", "on-new", "mmdc"]);
33927
+ var SHORT_PATH_CLUSTER = /^-(?!-)[A-Za-z]*[of]/;
33928
+ var LONG_FLAG = /^--([A-Za-z0-9][A-Za-z0-9-]*)(?:=([\s\S]*))?$/;
33929
+ function isPathFlag(service, name) {
33930
+ if (ID_FLAGS[service]?.has(name)) return false;
33931
+ return PATH_FLAGS.has(name) || name.endsWith("-file");
33932
+ }
33933
+ function confineFlagValue(flag, value) {
33934
+ if (value === "-") return;
33935
+ confinePath(value, flag);
33936
+ for (const part of value.split(",")) {
33937
+ if (part !== "" && part !== "-") confinePath(part, flag);
33938
+ }
33939
+ }
33940
+ function positionalPathTool(service, subcommand) {
33941
+ return POSITIONAL_PATH_SUBCOMMANDS[service]?.[subcommand];
33942
+ }
33943
+ function assertRunPathsConfined(service, subcommand, args) {
33944
+ const dedicated = positionalPathTool(service, subcommand);
33945
+ if (dedicated) {
33946
+ throw new Error(
33947
+ `gog ${service} ${subcommand} reads or writes a local path given as a positional argument, so it is not available through gog_${service}_run. Use ${dedicated}, which confines the path to GOG_FILE_ROOTS.`
33948
+ );
33949
+ }
33950
+ for (let i = 0; i < args.length; i++) {
33951
+ const arg = args[i];
33952
+ if (SHORT_PATH_CLUSTER.test(arg)) {
33953
+ throw new Error(
33954
+ `The flag ${arg} is not allowed here: spell a path flag out as --out or --file (e.g. --out=<path>) so its path can be checked against GOG_FILE_ROOTS.`
33955
+ );
33956
+ }
33957
+ const match = LONG_FLAG.exec(arg);
33958
+ if (!match) continue;
33959
+ const name = match[1].toLowerCase();
33960
+ if (EXEC_FLAGS.has(name)) {
33961
+ throw new Error(`The flag --${name} is not allowed here: it runs a local program chosen by the caller.`);
33962
+ }
33963
+ const isJson = name.endsWith("-json");
33964
+ if (!isJson && !isPathFlag(service, name)) continue;
33965
+ let value = match[2];
33966
+ if (value === void 0) {
33967
+ value = args[i + 1];
33968
+ if (value === void 0) continue;
33969
+ i++;
33970
+ }
33971
+ if (isJson) {
33972
+ if (value.startsWith("@")) confineFlagValue(`--${name}`, value.slice(1));
33973
+ continue;
33974
+ }
33975
+ confineFlagValue(`--${name}`, value);
33976
+ }
33977
+ }
33978
+
33568
33979
  // src/tools/utils.ts
33569
33980
  var PAYLOAD_INLINE_MAX = 4096;
33570
33981
  function payloadArg(inlineFlag, fileFlag, value, ext) {
@@ -33622,9 +34033,17 @@ function pushPaginationFlags(args, p) {
33622
34033
  if (p.all) args.push("--all");
33623
34034
  }
33624
34035
  function registerRunTool(server, options) {
33625
- const { service, examples, omitAccount = false, note } = options;
34036
+ const { service, examples, omitAccount = false, note, allowedSubcommands, vet, gmailNoSend = false } = options;
34037
+ for (const [, example] of examples.matchAll(/"([^"]+)"/g)) {
34038
+ const refused = allowedSubcommands && !allowedSubcommands.includes(example) || vet?.(example, []) || positionalPathTool(service, example);
34039
+ if (refused) {
34040
+ throw new Error(`gog_${service}_run example "${example}" is refused by the tool itself; list a subcommand it runs.`);
34041
+ }
34042
+ }
33626
34043
  const baseDescription = `Run any gog ${service} subcommand not covered by the other tools. Run \`gog ${service} --help\` for the full list of subcommands, or \`gog ${service} <subcommand> --help\` for flags on a specific subcommand.`;
33627
- const description = note ? `${baseDescription} ${note}` : baseDescription;
34044
+ const restriction = allowedSubcommands ? ` Only these subcommands are available: ${allowedSubcommands.join(", ")}.` : "";
34045
+ const safety = ' Flags that override server safety controls (--readonly, --enable-commands, --disable-commands, --account, --access-token, --home, --client, --gmail-no-send, --no-input) and a bare "--" are refused. Local file paths (--out, --out-dir, --attach, --file, --*-file, @file JSON) must be inside GOG_FILE_ROOTS; subcommands that take a local path positionally (e.g. drive upload) are refused in favour of their dedicated tools.';
34046
+ const description = `${baseDescription}${restriction}${safety}${note ? ` ${note}` : ""}`;
33628
34047
  const inputSchema = {
33629
34048
  subcommand: external_exports.string().describe(`The gog ${service} subcommand to run, e.g. ${examples}`),
33630
34049
  args: external_exports.array(external_exports.string()).describe("Additional positional args and flags")
@@ -33638,7 +34057,21 @@ function registerRunTool(server, options) {
33638
34057
  inputSchema: external_exports.object(inputSchema)
33639
34058
  }, async (rawArgs) => {
33640
34059
  const { subcommand, args, account } = rawArgs;
33641
- return runOrDiagnose([service, subcommand, ...args], { account });
34060
+ try {
34061
+ assertSafeSubcommand(subcommand);
34062
+ assertSafeForwardedArgs(args);
34063
+ if (allowedSubcommands && !allowedSubcommands.includes(subcommand)) {
34064
+ throw new Error(
34065
+ `gog ${service} ${subcommand} is not available through gog_${service}_run. Allowed: ${allowedSubcommands.join(", ")}.`
34066
+ );
34067
+ }
34068
+ const refusal = vet?.(subcommand, args);
34069
+ if (refusal) throw new Error(refusal);
34070
+ assertRunPathsConfined(service, subcommand, args);
34071
+ } catch (err) {
34072
+ return errorResult(errorText(err));
34073
+ }
34074
+ return runOrDiagnose([service, subcommand, ...args], gmailNoSend ? { account, gmailNoSend } : { account });
33642
34075
  });
33643
34076
  }
33644
34077
  function errorText(err) {
@@ -33766,7 +34199,240 @@ function assertNotBoth(inlineParam, fileParam, inlineValue, fileValue) {
33766
34199
  }
33767
34200
  }
33768
34201
 
34202
+ // src/dispatch-confirmation.ts
34203
+ import { createHash as createHash3 } from "node:crypto";
34204
+ import { readFileSync } from "node:fs";
34205
+
34206
+ // src/send-confirm-token.ts
34207
+ import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
34208
+ var CONFIRM_TOKEN_TTL_DEFAULT_SECONDS = 600;
34209
+ var PREFIX = "gct1";
34210
+ var secret;
34211
+ var spent = /* @__PURE__ */ new Map();
34212
+ function sendConfirmFallbackEnabled() {
34213
+ return readEnvVar("GOG_SEND_CONFIRM_FALLBACK")?.trim().toLowerCase() === "token";
34214
+ }
34215
+ function confirmTokenTtlSeconds() {
34216
+ const raw = readEnvVar("GOG_CONFIRM_TTL_SECONDS")?.trim();
34217
+ if (raw && /^\d+$/.test(raw)) {
34218
+ const n = Number(raw);
34219
+ if (n > 0) return n;
34220
+ }
34221
+ return CONFIRM_TOKEN_TTL_DEFAULT_SECONDS;
34222
+ }
34223
+ function hmacKey() {
34224
+ if (!secret) {
34225
+ const configured = readEnvVar("GOG_CONFIRM_SECRET");
34226
+ secret = configured ? Buffer.from(configured, "utf8") : randomBytes(32);
34227
+ }
34228
+ return secret;
34229
+ }
34230
+ function canonicalize(value) {
34231
+ if (Array.isArray(value)) return value.map(canonicalize);
34232
+ if (value && typeof value === "object") {
34233
+ const out = {};
34234
+ for (const key of Object.keys(value).sort()) {
34235
+ const v = value[key];
34236
+ if (v !== void 0) out[key] = canonicalize(v);
34237
+ }
34238
+ return out;
34239
+ }
34240
+ return value;
34241
+ }
34242
+ function hashSendPayload(payload) {
34243
+ return createHash2("sha256").update(JSON.stringify(canonicalize(payload))).digest("hex");
34244
+ }
34245
+ function sign(body) {
34246
+ return createHmac2("sha256", hmacKey()).update(`${PREFIX}.${body}`).digest("base64url");
34247
+ }
34248
+ function issueConfirmToken(binding, now = Date.now()) {
34249
+ const exp = now + confirmTokenTtlSeconds() * 1e3;
34250
+ const claims = {
34251
+ t: binding.tool,
34252
+ a: binding.account,
34253
+ g: binding.target,
34254
+ ...binding.revision === void 0 ? {} : { r: binding.revision },
34255
+ h: binding.payloadHash,
34256
+ iat: now,
34257
+ exp,
34258
+ n: randomBytes(16).toString("base64url")
34259
+ };
34260
+ const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
34261
+ return { token: `${PREFIX}.${body}.${sign(body)}`, expiresAt: new Date(exp).toISOString() };
34262
+ }
34263
+ function signatureMatches(body, sig) {
34264
+ const expected = Buffer.from(sign(body), "utf8");
34265
+ const actual = Buffer.from(sig, "utf8");
34266
+ return expected.length === actual.length && timingSafeEqual2(expected, actual);
34267
+ }
34268
+ function parseClaims(body) {
34269
+ try {
34270
+ return JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
34271
+ } catch {
34272
+ return void 0;
34273
+ }
34274
+ }
34275
+ function verifyConfirmToken(token, binding, now = Date.now()) {
34276
+ for (const [nonce, exp] of spent) if (exp < now) spent.delete(nonce);
34277
+ const parts = token.split(".");
34278
+ if (parts.length !== 3 || parts[0] !== PREFIX || !parts[1] || !parts[2]) return { ok: false, error: "TOKEN_INVALID" };
34279
+ const [, body, sig] = parts;
34280
+ if (!signatureMatches(body, sig)) return { ok: false, error: "TOKEN_INVALID" };
34281
+ const claims = parseClaims(body);
34282
+ if (!claims) return { ok: false, error: "TOKEN_INVALID" };
34283
+ if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
34284
+ return { ok: false, error: "TOKEN_INVALID" };
34285
+ }
34286
+ if (spent.has(claims.n)) return { ok: false, error: "TOKEN_REUSED" };
34287
+ if (now > claims.exp) return { ok: false, error: "TOKEN_EXPIRED" };
34288
+ if (claims.r !== binding.revision) return { ok: false, error: "DRAFT_CHANGED", reason: "message-id-rotated" };
34289
+ if (claims.h !== binding.payloadHash) return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
34290
+ spent.set(claims.n, claims.exp);
34291
+ return { ok: true };
34292
+ }
34293
+
34294
+ // src/dispatch-confirmation.ts
34295
+ var BODY_PREVIEW_MAX = 2048;
34296
+ function bodyPreview(text) {
34297
+ if (!text) return void 0;
34298
+ if (text.length <= BODY_PREVIEW_MAX) return text;
34299
+ return `${text.slice(0, BODY_PREVIEW_MAX)}\u2026 [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
34300
+ }
34301
+ function attachmentNames(paths, inline) {
34302
+ return [...paths ?? [], ...(inline ?? []).map((a) => a.filename)];
34303
+ }
34304
+ function attachmentDetails(paths, inline) {
34305
+ const out = [];
34306
+ for (const path of paths ?? []) {
34307
+ let bytes;
34308
+ try {
34309
+ bytes = readFileSync(path);
34310
+ } catch {
34311
+ bytes = void 0;
34312
+ }
34313
+ out.push(bytes ? { name: path, size: bytes.length, sha256: createHash3("sha256").update(bytes).digest("hex") } : { name: path, size: null });
34314
+ }
34315
+ for (const a of inline ?? []) {
34316
+ out.push({
34317
+ name: a.filename,
34318
+ size: Buffer.from(a.contentBase64, "base64").length,
34319
+ sha256: createHash3("sha256").update(a.contentBase64).digest("hex")
34320
+ });
34321
+ }
34322
+ return out;
34323
+ }
34324
+ function senderPreview(account, from) {
34325
+ return from ?? account ?? readEnvVar("GOG_ACCOUNT") ?? "the gog account's default address";
34326
+ }
34327
+ function attachmentPreview(details) {
34328
+ return details.map(({ name, size }) => ({ name, size }));
34329
+ }
34330
+ 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.";
34331
+ var CONFIRM_ACTION_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
34332
+ var FALLBACK_HINT = "Or set GOG_SEND_CONFIRM_FALLBACK=token to enable two-step confirmation.";
34333
+ var CONFIRM_FALLBACK_DESCRIPTION = ' If the client cannot show that prompt (no MCP elicitation, e.g. claude.ai) and the server sets GOG_SEND_CONFIRM_FALLBACK=token, 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. Show that preview to the user verbatim; 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.';
34334
+ var confirmTokenParam = external_exports.string().optional().describe(
34335
+ `ONLY for the two-step fallback (client without MCP elicitation, server with GOG_SEND_CONFIRM_FALLBACK=token). 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.`
34336
+ );
34337
+ var TOKEN_ERROR_NOTE = {
34338
+ 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.",
34339
+ 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.",
34340
+ 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."
34341
+ };
34342
+ var DRAFT_CHANGED_NOTE = {
34343
+ "message-id-rotated": "Nothing was sent or changed: the target was edited since the user approved it (a draft's messageId or an event's version rotated), so what would happen is not what they saw.",
34344
+ "payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
34345
+ };
34346
+ function isToolResult(value) {
34347
+ return Array.isArray(value.content);
34348
+ }
34349
+ function rejection(data) {
34350
+ return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
34351
+ }
34352
+ async function tokenConfirmation(op, fallback) {
34353
+ const subject = await fallback.subject();
34354
+ if (isToolResult(subject)) return subject;
34355
+ const binding = {
34356
+ tool: fallback.tool,
34357
+ account: fallback.account ?? readEnvVar("GOG_ACCOUNT") ?? "",
34358
+ target: subject.target,
34359
+ ...subject.revision === void 0 ? {} : { revision: subject.revision },
34360
+ payloadHash: hashSendPayload(subject.payload)
34361
+ };
34362
+ const phaseOne = () => {
34363
+ const { token, expiresAt } = issueConfirmToken(binding);
34364
+ return {
34365
+ action: op,
34366
+ preview: subject.preview,
34367
+ confirmToken: token,
34368
+ expiresAt,
34369
+ ttlSeconds: confirmTokenTtlSeconds(),
34370
+ instruction: fallback.instruction ?? CONFIRM_ACTION_INSTRUCTION
34371
+ };
34372
+ };
34373
+ if (!fallback.confirmToken) {
34374
+ return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
34375
+ }
34376
+ const verdict = verifyConfirmToken(fallback.confirmToken, binding);
34377
+ if (verdict.ok) return void 0;
34378
+ if (verdict.error === "DRAFT_CHANGED") {
34379
+ return rejection({
34380
+ error: "DRAFT_CHANGED",
34381
+ reason: verdict.reason,
34382
+ note: `${DRAFT_CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
34383
+ ...phaseOne()
34384
+ });
34385
+ }
34386
+ return rejection({ error: verdict.error, action: op, note: TOKEN_ERROR_NOTE[verdict.error] });
34387
+ }
34388
+ function gatedElsewhere(what, via, does, tool) {
34389
+ return `${what} ${does} and is not available through ${via}. Use ${tool}, which asks the user to confirm.`;
34390
+ }
34391
+ function hasCommandWord(args, words) {
34392
+ return args.find((a) => words.has(a.toLowerCase()));
34393
+ }
34394
+ async function requireDispatchConfirmation(ctx, options) {
34395
+ const { action, fallback } = options;
34396
+ if (fallback && callerAcceptsFormElicitation(ctx) === false && sendConfirmFallbackEnabled()) {
34397
+ return tokenConfirmation(action, fallback);
34398
+ }
34399
+ const note = fallback ? [options.unsupportedNote, FALLBACK_HINT].filter(Boolean).join(" ") : options.unsupportedNote;
34400
+ return requireConfirmation(ctx, {
34401
+ action,
34402
+ message: options.message,
34403
+ details: options.details,
34404
+ confirmationLabel: options.confirmationLabel,
34405
+ ...note ? { unsupportedNote: note } : {}
34406
+ });
34407
+ }
34408
+ function resultText(result) {
34409
+ const first = result.content[0];
34410
+ return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
34411
+ }
34412
+
33769
34413
  // src/tools/api.ts
34414
+ var GMAIL_API_BLOCKED = /(?:\.send$|forwarding|filters\.create|filters\.update|delegates\.create)/i;
34415
+ var DISPATCH_API_BLOCKED = {
34416
+ chat: [{ method: /(?:^|\.)spaces\.messages\.create$/i, does: "posts a Chat message", tool: "gog_chat_messages_send / gog_chat_dm_send" }],
34417
+ drive: [{ method: /(?:^|\.)permissions\.(?:create|update)$/i, does: "grants access to a file", tool: "gog_drive_share" }],
34418
+ classroom: [
34419
+ { method: /(?:^|\.)courses\.announcements\.create$/i, does: "posts to a class", tool: "gog_classroom_announcements_create" },
34420
+ { method: /(?:^|\.)invitations\.create$/i, does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
34421
+ ],
34422
+ calendar: [
34423
+ { method: /(?:^|\.)events\.(?:insert|import|quickadd)$/i, does: "can put an event on guests' calendars", tool: "gog_calendar_create" },
34424
+ { method: /(?:^|\.)events\.(?:update|patch)$/i, does: "can change what guests see", tool: "gog_calendar_update / gog_calendar_respond" }
34425
+ ]
34426
+ };
34427
+ function refusedApiCall(api, method) {
34428
+ const name = api.trim().toLowerCase();
34429
+ const m = method.trim();
34430
+ if (name === "gmail" && GMAIL_API_BLOCKED.test(m)) {
34431
+ 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.`;
34432
+ }
34433
+ const hit = (Object.hasOwn(DISPATCH_API_BLOCKED, name) ? DISPATCH_API_BLOCKED[name] : []).find((b) => b.method.test(m));
34434
+ return hit ? gatedElsewhere(`${name} ${m}`, "gog_api_call", hit.does, hit.tool) : void 0;
34435
+ }
33770
34436
  function registerApiTools(server) {
33771
34437
  server.registerTool("gog_api_list", {
33772
34438
  description: "List the Google Discovery APIs available for gog_api_call / gog_api_describe (name + version + title).",
@@ -33790,12 +34456,12 @@ function registerApiTools(server) {
33790
34456
  account: accountParam
33791
34457
  })
33792
34458
  }, async ({ api, version: version2, method, account }) => {
33793
- const args = ["api", "describe", api, version2];
33794
- if (method) args.push(method);
34459
+ const args = ["api", "describe", pos(api), pos(version2)];
34460
+ if (method) args.push(pos(method));
33795
34461
  return runOrDiagnose(args, { account });
33796
34462
  });
33797
34463
  server.registerTool("gog_api_call", {
33798
- 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.",
34464
+ 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.",
33799
34465
  annotations: { destructiveHint: true },
33800
34466
  inputSchema: external_exports.object({
33801
34467
  api: external_exports.string().describe("Discovery API name (e.g. drive, gmail, calendar)"),
@@ -33809,14 +34475,23 @@ function registerApiTools(server) {
33809
34475
  account: accountParam
33810
34476
  })
33811
34477
  }, async ({ api, version: version2, method, params, body, scope, allowWrite, dryRun, account }) => {
33812
- const args = ["api", "call", api, version2, method];
34478
+ try {
34479
+ assertSafeForwardedArgs([api, version2, method]);
34480
+ const refusal = refusedApiCall(api, method);
34481
+ if (refusal) throw new Error(refusal);
34482
+ if (body) confineAtFile(body, "body");
34483
+ if (params) confineAtFile(params, "params");
34484
+ } catch (err) {
34485
+ return errorResult(errorText(err));
34486
+ }
34487
+ const args = ["api", "call", pos(api), pos(version2), pos(method)];
33813
34488
  if (params) args.push(`--params=${params}`);
33814
34489
  if (body) args.push(`--body=${body}`);
33815
34490
  if (scope) args.push(`--scope=${scope}`);
33816
34491
  if (allowWrite) args.push("--allow-write");
33817
34492
  if (dryRun) args.push("--dry-run");
33818
34493
  if (allowWrite) args.push("--force");
33819
- return runOrDiagnose(args, { account });
34494
+ return runOrDiagnose(args, { account, gmailNoSend: true });
33820
34495
  });
33821
34496
  }
33822
34497
 
@@ -33834,7 +34509,7 @@ function registerAppScriptTools(server) {
33834
34509
  account: accountParam
33835
34510
  })
33836
34511
  }, async ({ scriptId, account }) => {
33837
- return runOrDiagnose(["appscript", "get", scriptId], { account });
34512
+ return runOrDiagnose(["appscript", "get", pos(scriptId)], { account });
33838
34513
  });
33839
34514
  server.registerTool("gog_appscript_content", {
33840
34515
  description: `Read a project's source \u2014 every .gs file and its appsscript.json manifest \u2014 INLINE in the response. This is the tool to reach for when the question is "what does this script do"; it needs no filesystem, so it works the same on a hosted deployment as it does locally, unlike gog_appscript_pull.` + apiEnableNote,
@@ -33844,19 +34519,20 @@ function registerAppScriptTools(server) {
33844
34519
  account: accountParam
33845
34520
  })
33846
34521
  }, async ({ scriptId, account }) => {
33847
- return runOrDiagnose(["appscript", "content", scriptId], { account });
34522
+ return runOrDiagnose(["appscript", "content", pos(scriptId)], { account });
33848
34523
  });
33849
34524
  server.registerTool("gog_appscript_pull", {
33850
34525
  description: "Write a project's files into a local directory, for editing a script as ordinary files. THE DIRECTORY IS RESOLVED WHERE GOG RUNS, which is the caller's own machine only on a local (stdio) deployment: on a hosted deployment (e.g. mcp-host) the files land on that server where the caller cannot reach them. Use gog_appscript_content there instead \u2014 it returns the same source in the response. Existing files are left alone unless overwrite is set. Read-only as far as Google is concerned: nothing is pushed back." + apiEnableNote,
33851
34526
  annotations: { destructiveHint: true },
33852
34527
  inputSchema: external_exports.object({
33853
34528
  scriptId: scriptIdParam,
33854
- dir: external_exports.string().describe("Destination directory, resolved on the machine where gog runs"),
34529
+ dir: external_exports.string().describe("Destination directory, resolved on the machine where gog runs; must be inside the server's GOG_FILE_ROOTS directories"),
33855
34530
  overwrite: external_exports.boolean().optional().describe("Overwrite files that already exist in dir"),
33856
34531
  account: accountParam
33857
34532
  })
33858
34533
  }, async ({ scriptId, dir, overwrite, account }) => {
33859
- const args = ["appscript", "pull", scriptId, dir];
34534
+ confinePath(dir, "dir");
34535
+ const args = ["appscript", "pull", pos(scriptId), pos(dir)];
33860
34536
  if (overwrite) args.push("--overwrite");
33861
34537
  return runOrDiagnose(args, { account });
33862
34538
  });
@@ -33882,7 +34558,7 @@ function registerAppScriptTools(server) {
33882
34558
  account: accountParam
33883
34559
  })
33884
34560
  }, async ({ scriptId, max, pageToken, page, all, account }) => {
33885
- const args = ["appscript", "deployments", scriptId];
34561
+ const args = ["appscript", "deployments", pos(scriptId)];
33886
34562
  pushPaginationFlags(args, { max, pageToken, page, all });
33887
34563
  return runOrDiagnose(args, { account });
33888
34564
  });
@@ -33895,7 +34571,7 @@ function registerAppScriptTools(server) {
33895
34571
  account: accountParam
33896
34572
  })
33897
34573
  }, async ({ scriptId, max, pageToken, page, all, account }) => {
33898
- const args = ["appscript", "versions", scriptId];
34574
+ const args = ["appscript", "versions", pos(scriptId)];
33899
34575
  pushPaginationFlags(args, { max, pageToken, page, all });
33900
34576
  return runOrDiagnose(args, { account });
33901
34577
  });
@@ -33921,7 +34597,7 @@ function registerAppScriptTools(server) {
33921
34597
  throw new Error(`params must be a JSON ARRAY of positional arguments, e.g. '["a", 1]' \u2014 Apps Script takes positional arguments, not named ones. Received: ${params}`);
33922
34598
  }
33923
34599
  }
33924
- const args = ["appscript", "run", scriptId, functionName];
34600
+ const args = ["appscript", "run", pos(scriptId), pos(functionName)];
33925
34601
  if (params !== void 0) args.push(`--params=${params}`);
33926
34602
  if (devMode) args.push("--dev-mode");
33927
34603
  return runOrDiagnose(args, { account });
@@ -33934,6 +34610,7 @@ function registerAppScriptTools(server) {
33934
34610
  }
33935
34611
 
33936
34612
  // src/tools/auth.ts
34613
+ var AUTH_RUN_SUBCOMMANDS = ["list", "status", "services", "remove", "alias"];
33937
34614
  function registerAuthToolsWith(server, defaultServices) {
33938
34615
  const servicesDescribe = `Services to authorize: "all" or comma-separated list (e.g. "sheets,gmail,calendar"). Default: "${defaultServices}". Prefer the narrowest set you need \u2014 requesting a service whose Google API is not enabled on the OAuth client's project makes Google reject the WHOLE request with invalid_scope.`;
33939
34616
  const extraScopesDescribe = "Additional raw OAuth scope URIs to request, comma-separated, on top of the ones `services` implies. Use for scopes no service covers \u2014 e.g. https://www.googleapis.com/auth/bigquery.readonly, required before gog_sheets_datasource_* can read BigQuery-backed Connected Sheets. Leave unset otherwise: an extra scope whose API is not enabled on the OAuth client project makes Google reject the WHOLE authorization with invalid_scope.";
@@ -33991,7 +34668,7 @@ function registerAuthToolsWith(server, defaultServices) {
33991
34668
  })
33992
34669
  }, async ({ email: email3, services = defaultServices, extraScopes }) => {
33993
34670
  try {
33994
- const args = ["auth", "add", email3, "--services", services];
34671
+ const args = ["auth", "add", pos(email3), "--services", services];
33995
34672
  if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, "--force-consent");
33996
34673
  return rawTextResult(await run(args, {
33997
34674
  interactive: true,
@@ -34011,7 +34688,7 @@ function registerAuthToolsWith(server, defaultServices) {
34011
34688
  })
34012
34689
  }, async ({ email: email3, services = defaultServices, extraScopes }) => {
34013
34690
  try {
34014
- const args = ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"];
34691
+ const args = ["auth", "add", pos(email3), "--remote", "--step", "1", "--services", services, "--force-consent"];
34015
34692
  if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
34016
34693
  return rawTextResult(await run(args, { redactMode: "tokens" }));
34017
34694
  } catch (err) {
@@ -34038,7 +34715,7 @@ function registerAuthToolsWith(server, defaultServices) {
34038
34715
  const args = [
34039
34716
  "auth",
34040
34717
  "add",
34041
- email3,
34718
+ pos(email3),
34042
34719
  "--remote",
34043
34720
  "--step",
34044
34721
  "2",
@@ -34058,6 +34735,7 @@ function registerAuthToolsWith(server, defaultServices) {
34058
34735
  service: "auth",
34059
34736
  examples: '"remove", "alias", "list"',
34060
34737
  omitAccount: true,
34738
+ allowedSubcommands: AUTH_RUN_SUBCOMMANDS,
34061
34739
  note: "For browser-based authorization, use gog_auth_add instead."
34062
34740
  });
34063
34741
  }
@@ -34069,6 +34747,54 @@ function authToolsFor(defaultServices) {
34069
34747
  }
34070
34748
 
34071
34749
  // src/tools/calendar.ts
34750
+ var CALENDAR_GATED = {
34751
+ create: "gog_calendar_create",
34752
+ add: "gog_calendar_create",
34753
+ new: "gog_calendar_create",
34754
+ update: "gog_calendar_update",
34755
+ edit: "gog_calendar_update",
34756
+ set: "gog_calendar_update",
34757
+ respond: "gog_calendar_respond",
34758
+ rsvp: "gog_calendar_respond",
34759
+ reply: "gog_calendar_respond"
34760
+ };
34761
+ function vetCalendarRun(subcommand, _args) {
34762
+ const sub = subcommand.toLowerCase();
34763
+ const tool = Object.hasOwn(CALENDAR_GATED, sub) ? CALENDAR_GATED[sub] : void 0;
34764
+ return tool ? gatedElsewhere(`gog calendar ${sub}`, "gog_calendar_run", "can change what guests see", tool) : void 0;
34765
+ }
34766
+ function parseAttendees(value) {
34767
+ if (!value) return [];
34768
+ 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);
34769
+ }
34770
+ function timeOf(t) {
34771
+ const v = t?.dateTime ?? t?.date;
34772
+ return typeof v === "string" ? v : void 0;
34773
+ }
34774
+ function eventSnapshot(raw) {
34775
+ let event;
34776
+ try {
34777
+ event = JSON.parse(raw)?.event;
34778
+ } catch {
34779
+ event = void 0;
34780
+ }
34781
+ const str = (v) => typeof v === "string" ? v : void 0;
34782
+ return {
34783
+ summary: str(event?.summary),
34784
+ start: timeOf(event?.start),
34785
+ end: timeOf(event?.end),
34786
+ organizer: str(event?.organizer?.email),
34787
+ // Guests are the OTHER people: not this account's own attendee row, not rooms.
34788
+ guests: (Array.isArray(event?.attendees) ? event.attendees : []).filter((a) => a.self !== true && a.resource !== true && typeof a.email === "string").map((a) => a.email),
34789
+ etag: str(event?.etag)
34790
+ };
34791
+ }
34792
+ async function readEvent(calendarId, eventId, account) {
34793
+ const got = await runOrDiagnose(["calendar", "event", pos(calendarId), pos(eventId)], { account });
34794
+ if (got.isError) return { error: got };
34795
+ return { event: eventSnapshot(resultText(got)) };
34796
+ }
34797
+ var CALENDAR_CONFIRM_LABEL = "Confirm that this change, which other people will see, should be made now.";
34072
34798
  var reminderParams = {
34073
34799
  reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
34074
34800
  `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.`
@@ -34122,7 +34848,7 @@ function registerCalendarTools(server) {
34122
34848
  })
34123
34849
  }, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, view, account }) => {
34124
34850
  const args = ["calendar", "events"];
34125
- if (calendarId) args.push(calendarId);
34851
+ if (calendarId) args.push(pos(calendarId));
34126
34852
  if (from) args.push(`--from=${from}`);
34127
34853
  if (to) args.push(`--to=${to}`);
34128
34854
  if (days !== void 0) args.push(`--days=${days}`);
@@ -34151,12 +34877,12 @@ function registerCalendarTools(server) {
34151
34877
  account: accountParam
34152
34878
  })
34153
34879
  }, async ({ calendarId, eventId, timezone, account }) => {
34154
- const args = ["calendar", "event", calendarId, eventId];
34880
+ const args = ["calendar", "event", pos(calendarId), pos(eventId)];
34155
34881
  if (timezone) args.push(`--timezone=${timezone}`);
34156
34882
  return runOrDiagnose(args, { account });
34157
34883
  });
34158
34884
  server.registerTool("gog_calendar_create", {
34159
- 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).",
34885
+ 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,
34160
34886
  annotations: { destructiveHint: false },
34161
34887
  inputSchema: external_exports.object({
34162
34888
  calendarId: external_exports.string().describe('Calendar ID (use "primary" for the default calendar)'),
@@ -34170,10 +34896,11 @@ function registerCalendarTools(server) {
34170
34896
  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."),
34171
34897
  withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)"),
34172
34898
  ...reminderParams,
34173
- account: accountParam
34899
+ account: accountParam,
34900
+ confirmToken: confirmTokenParam
34174
34901
  })
34175
- }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
34176
- const args = ["calendar", "create", calendarId, `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
34902
+ }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account, confirmToken }, ctx) => {
34903
+ const args = ["calendar", "create", pos(calendarId), `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
34177
34904
  if (description) args.push(`--description=${description}`);
34178
34905
  if (location) args.push(`--location=${location}`);
34179
34906
  if (attendees) args.push(`--attendees=${attendees}`);
@@ -34181,10 +34908,28 @@ function registerCalendarTools(server) {
34181
34908
  if (timezone) args.push(`--timezone=${timezone}`);
34182
34909
  if (withZoom) args.push("--with-zoom");
34183
34910
  pushReminderFlags(args, { reminders, noReminders });
34911
+ const guests = parseAttendees(attendees);
34912
+ if (guests.length > 0) {
34913
+ const event = { calendarId, summary, from, to, allDay: Boolean(allDay), timezone, location, description, guests, attendees, withZoom: Boolean(withZoom) };
34914
+ const confirmation = await requireDispatchConfirmation(ctx, {
34915
+ action: "calendar.create",
34916
+ message: "Review and confirm this event, which will appear on the guests' calendars:",
34917
+ confirmationLabel: CALENDAR_CONFIRM_LABEL,
34918
+ details: event,
34919
+ unsupportedNote: "Create it without attendees instead; the user can add the guests in Google Calendar.",
34920
+ fallback: {
34921
+ tool: "gog_calendar_create",
34922
+ account,
34923
+ confirmToken,
34924
+ subject: () => ({ target: calendarId, payload: event, preview: event })
34925
+ }
34926
+ });
34927
+ if (confirmation) return confirmation;
34928
+ }
34184
34929
  return runOrDiagnose(args, { account });
34185
34930
  });
34186
34931
  server.registerTool("gog_calendar_update", {
34187
- 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.",
34932
+ 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,
34188
34933
  annotations: { destructiveHint: false },
34189
34934
  inputSchema: external_exports.object({
34190
34935
  calendarId: external_exports.string().describe("Calendar ID"),
@@ -34202,10 +34947,11 @@ function registerCalendarTools(server) {
34202
34947
  removeZoom: external_exports.boolean().optional().describe("Remove the event's Zoom video conference"),
34203
34948
  removeMeet: external_exports.boolean().optional().describe("Remove the event's Google Meet video conference (clears conference data only)"),
34204
34949
  ...reminderParams,
34205
- account: accountParam
34950
+ account: accountParam,
34951
+ confirmToken: confirmTokenParam
34206
34952
  })
34207
- }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
34208
- const args = ["calendar", "update", calendarId, eventId];
34953
+ }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account, confirmToken }, ctx) => {
34954
+ const args = ["calendar", "update", pos(calendarId), pos(eventId)];
34209
34955
  if (summary !== void 0) args.push(`--summary=${summary}`);
34210
34956
  if (from !== void 0) args.push(`--from=${from}`);
34211
34957
  if (to !== void 0) args.push(`--to=${to}`);
@@ -34219,6 +34965,45 @@ function registerCalendarTools(server) {
34219
34965
  if (removeZoom) args.push("--remove-zoom");
34220
34966
  if (removeMeet) args.push("--remove-meet");
34221
34967
  pushReminderFlags(args, { reminders, noReminders });
34968
+ const changes = Object.fromEntries(Object.entries({
34969
+ summary,
34970
+ from,
34971
+ to,
34972
+ description,
34973
+ location,
34974
+ attendees,
34975
+ addAttendees,
34976
+ attachments,
34977
+ withZoom,
34978
+ regenerateZoom,
34979
+ removeZoom,
34980
+ removeMeet
34981
+ }).filter(([, v]) => v !== void 0 && v !== false));
34982
+ if (Object.keys(changes).length > 0) {
34983
+ const read = await readEvent(calendarId, eventId, account);
34984
+ if (read.error) return read.error;
34985
+ const { etag, ...current } = read.event;
34986
+ const added = [...parseAttendees(attendees), ...parseAttendees(addAttendees)];
34987
+ if (current.guests.length > 0 || added.length > 0) {
34988
+ const view = { calendarId, eventId, current, changes };
34989
+ const confirmation = await requireDispatchConfirmation(ctx, {
34990
+ action: "calendar.update",
34991
+ message: "Review and confirm this change to an event other people are on:",
34992
+ confirmationLabel: CALENDAR_CONFIRM_LABEL,
34993
+ details: view,
34994
+ unsupportedNote: "Ask the user to make this change in Google Calendar.",
34995
+ fallback: {
34996
+ tool: "gog_calendar_update",
34997
+ account,
34998
+ confirmToken,
34999
+ // The etag rotates on any edit, so an event changed elsewhere between
35000
+ // the two phases is DRAFT_CHANGED rather than edited over.
35001
+ subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
35002
+ }
35003
+ });
35004
+ if (confirmation) return confirmation;
35005
+ }
35006
+ }
34222
35007
  return runOrDiagnose(args, { account });
34223
35008
  });
34224
35009
  server.registerTool("gog_calendar_delete", {
@@ -34230,24 +35015,43 @@ function registerCalendarTools(server) {
34230
35015
  account: accountParam
34231
35016
  })
34232
35017
  }, async ({ calendarId, eventId, account }) => {
34233
- return runOrDiagnose(["calendar", "delete", calendarId, eventId, "--force"], { account });
35018
+ return runOrDiagnose(["calendar", "delete", pos(calendarId), pos(eventId), "--force"], { account });
34234
35019
  });
34235
35020
  server.registerTool("gog_calendar_respond", {
34236
- description: "Respond to a calendar event invitation.",
35021
+ 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,
34237
35022
  annotations: { destructiveHint: true },
34238
35023
  inputSchema: external_exports.object({
34239
35024
  calendarId: external_exports.string().describe("Calendar ID"),
34240
35025
  eventId: external_exports.string().describe("Event ID"),
34241
35026
  status: external_exports.enum(["accepted", "declined", "tentative"]).describe("Response status"),
34242
35027
  comment: external_exports.string().optional().describe("Optional comment to include with response"),
34243
- account: accountParam
35028
+ account: accountParam,
35029
+ confirmToken: confirmTokenParam
34244
35030
  })
34245
- }, async ({ calendarId, eventId, status, comment, account }) => {
34246
- const args = ["calendar", "respond", calendarId, eventId, `--status=${status}`];
35031
+ }, async ({ calendarId, eventId, status, comment, account, confirmToken }, ctx) => {
35032
+ const args = ["calendar", "respond", pos(calendarId), pos(eventId), `--status=${status}`];
34247
35033
  if (comment) args.push(`--comment=${comment}`);
35034
+ const read = await readEvent(calendarId, eventId, account);
35035
+ if (read.error) return read.error;
35036
+ const { etag, ...event } = read.event;
35037
+ const view = { calendarId, eventId, event, response: status, ...comment ? { comment } : {} };
35038
+ const confirmation = await requireDispatchConfirmation(ctx, {
35039
+ action: "calendar.respond",
35040
+ message: "Review and confirm this response, which the organizer will see:",
35041
+ confirmationLabel: "Confirm that this response should be recorded now.",
35042
+ details: view,
35043
+ unsupportedNote: "Ask the user to respond from Google Calendar.",
35044
+ fallback: {
35045
+ tool: "gog_calendar_respond",
35046
+ account,
35047
+ confirmToken,
35048
+ subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
35049
+ }
35050
+ });
35051
+ if (confirmation) return confirmation;
34248
35052
  return runOrDiagnose(args, { account });
34249
35053
  });
34250
- registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"' });
35054
+ registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"', vet: vetCalendarRun });
34251
35055
  }
34252
35056
 
34253
35057
  // src/attachments.ts
@@ -34258,6 +35062,7 @@ var MAX_REQUEST_PAYLOAD_WIRE_BYTES = REQUEST_MAX_BODY_BYTES - REQUEST_BODY_JSON_
34258
35062
  var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
34259
35063
  function wireBytesOf(arg) {
34260
35064
  if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
35065
+ if (arg.kind === "positional") return Buffer.byteLength(arg.value, "utf8");
34261
35066
  return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
34262
35067
  }
34263
35068
  var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
@@ -34330,6 +35135,23 @@ function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
34330
35135
  }
34331
35136
 
34332
35137
  // src/tools/chat.ts
35138
+ var CHAT_SEND_WORDS = /* @__PURE__ */ new Set(["send", "create", "post"]);
35139
+ function vetChatRun(subcommand, args) {
35140
+ const sub = subcommand.toLowerCase();
35141
+ if (sub !== "messages" && sub !== "dm") return void 0;
35142
+ const word = hasCommandWord(args, CHAT_SEND_WORDS);
35143
+ if (!word) return void 0;
35144
+ return gatedElsewhere(
35145
+ `gog chat ${sub} ${word.toLowerCase()}`,
35146
+ "gog_chat_run",
35147
+ "posts a message",
35148
+ sub === "dm" ? "gog_chat_dm_send" : "gog_chat_messages_send"
35149
+ );
35150
+ }
35151
+ var CHAT_CONFIRM = {
35152
+ message: "Review and confirm this Google Chat message:",
35153
+ confirmationLabel: "Confirm that this Chat message should be posted now."
35154
+ };
34333
35155
  function registerChatTools(server) {
34334
35156
  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.';
34335
35157
  const spaceParam = external_exports.string().describe(
@@ -34360,7 +35182,7 @@ function registerChatTools(server) {
34360
35182
  account: accountParam
34361
35183
  })
34362
35184
  }, async ({ displayName, exact, max, account }) => {
34363
- const args = ["chat", "spaces", "find", displayName];
35185
+ const args = ["chat", "spaces", "find", pos(displayName)];
34364
35186
  if (exact) args.push("--exact");
34365
35187
  if (max !== void 0) args.push(`--max=${max}`);
34366
35188
  return runOrDiagnose(args, { account });
@@ -34374,7 +35196,7 @@ function registerChatTools(server) {
34374
35196
  account: accountParam
34375
35197
  })
34376
35198
  }, async ({ displayName, members: members2, account }) => {
34377
- const args = ["chat", "spaces", "create", displayName];
35199
+ const args = ["chat", "spaces", "create", pos(displayName)];
34378
35200
  if (members2) for (const member of members2) args.push(`--member=${member}`);
34379
35201
  return runOrDiagnose(args, { account });
34380
35202
  });
@@ -34387,7 +35209,7 @@ function registerChatTools(server) {
34387
35209
  account: accountParam
34388
35210
  })
34389
35211
  }, async ({ space, max, pageToken, page, all, account }) => {
34390
- const args = ["chat", "threads", "list", space];
35212
+ const args = ["chat", "threads", "list", pos(space)];
34391
35213
  pushPaginationFlags(args, { max, pageToken, page, all });
34392
35214
  return runOrDiagnose(args, { account });
34393
35215
  });
@@ -34403,7 +35225,7 @@ function registerChatTools(server) {
34403
35225
  account: accountParam
34404
35226
  })
34405
35227
  }, async ({ space, thread, unread, order, max, pageToken, page, all, account }) => {
34406
- const args = ["chat", "messages", "list", space];
35228
+ const args = ["chat", "messages", "list", pos(space)];
34407
35229
  if (thread) args.push(`--thread=${thread}`);
34408
35230
  if (unread) args.push("--unread");
34409
35231
  if (order) args.push(`--order=${order}`);
@@ -34427,7 +35249,7 @@ function registerChatTools(server) {
34427
35249
  account: accountParam
34428
35250
  })
34429
35251
  }, async ({ query, order, view, markup, max, pageToken, page, all, account }) => {
34430
- const args = ["chat", "messages", "search", query];
35252
+ const args = ["chat", "messages", "search", pos(query)];
34431
35253
  if (order) args.push(`--order=${order}`);
34432
35254
  if (view) args.push(`--view=${view}`);
34433
35255
  if (markup) args.push(`--markup=${markup}`);
@@ -34435,41 +35257,79 @@ function registerChatTools(server) {
34435
35257
  return runOrDiagnose(args, { account });
34436
35258
  });
34437
35259
  server.registerTool("gog_chat_messages_send", {
34438
- 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,
35260
+ 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,
34439
35261
  annotations: { destructiveHint: true },
34440
35262
  inputSchema: external_exports.object({
34441
35263
  space: spaceParam,
34442
35264
  text: external_exports.string().optional().describe("Message text. Optional only when an attachment is supplied."),
34443
35265
  thread: threadParam,
34444
35266
  attach: external_exports.array(external_exports.string()).optional().describe(
34445
- "Attachment file paths, read WHERE GOG RUNS. On a hosted or remote deployment that is not your machine \u2014 use attachInline there instead."
35267
+ "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)."
34446
35268
  ),
34447
35269
  attachInline: attachInlineParam,
34448
- account: accountParam
35270
+ account: accountParam,
35271
+ confirmToken: confirmTokenParam
34449
35272
  })
34450
- }, async ({ space, text, thread, attach, attachInline, account }) => {
35273
+ }, async ({ space, text, thread, attach, attachInline, account, confirmToken }, ctx) => {
35274
+ confinePaths(attach, "attach");
34451
35275
  if (text === void 0 && !attach?.length && !attachInline?.length) {
34452
35276
  throw new Error("A Chat message needs text, an attachment, or both.");
34453
35277
  }
34454
- const args = ["chat", "messages", "send", space];
35278
+ const args = ["chat", "messages", "send", pos(space)];
34455
35279
  if (text !== void 0) args.push(`--text=${text}`);
34456
35280
  if (thread) args.push(`--thread=${thread}`);
34457
35281
  if (attach) for (const path of attach) args.push(`--attach=${path}`);
34458
35282
  args.push(...inlineAttachmentArgs("attach", attachInline, args));
35283
+ const confirmation = await requireDispatchConfirmation(ctx, {
35284
+ ...CHAT_CONFIRM,
35285
+ action: "chat.message-send",
35286
+ details: { space, thread, textPreview: bodyPreview(text), attachments: attachmentNames(attach, attachInline) },
35287
+ fallback: {
35288
+ tool: "gog_chat_messages_send",
35289
+ account,
35290
+ confirmToken,
35291
+ subject: () => {
35292
+ const attachments = attachmentDetails(attach, attachInline);
35293
+ const from = senderPreview(account);
35294
+ return {
35295
+ target: space,
35296
+ payload: { from, space, thread, text, attachments },
35297
+ preview: { from, space, thread, text, attachments: attachmentPreview(attachments) }
35298
+ };
35299
+ }
35300
+ }
35301
+ });
35302
+ if (confirmation) return confirmation;
34459
35303
  return runOrDiagnose(args, { account });
34460
35304
  });
34461
35305
  server.registerTool("gog_chat_dm_send", {
34462
- 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,
35306
+ 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,
34463
35307
  annotations: { destructiveHint: true },
34464
35308
  inputSchema: external_exports.object({
34465
35309
  email: external_exports.string().describe("Recipient email address"),
34466
35310
  text: external_exports.string().describe("Message text"),
34467
35311
  thread: threadParam,
34468
- account: accountParam
35312
+ account: accountParam,
35313
+ confirmToken: confirmTokenParam
34469
35314
  })
34470
- }, async ({ email: email3, text, thread, account }) => {
34471
- const args = ["chat", "dm", "send", email3, `--text=${text}`];
35315
+ }, async ({ email: email3, text, thread, account, confirmToken }, ctx) => {
35316
+ const args = ["chat", "dm", "send", pos(email3), `--text=${text}`];
34472
35317
  if (thread) args.push(`--thread=${thread}`);
35318
+ const confirmation = await requireDispatchConfirmation(ctx, {
35319
+ ...CHAT_CONFIRM,
35320
+ action: "chat.dm-send",
35321
+ details: { to: email3, thread, textPreview: bodyPreview(text) },
35322
+ fallback: {
35323
+ tool: "gog_chat_dm_send",
35324
+ account,
35325
+ confirmToken,
35326
+ subject: () => {
35327
+ const from = senderPreview(account);
35328
+ return { target: email3, payload: { from, to: email3, thread, text }, preview: { from, to: email3, thread, text } };
35329
+ }
35330
+ }
35331
+ });
35332
+ if (confirmation) return confirmation;
34473
35333
  return runOrDiagnose(args, { account });
34474
35334
  });
34475
35335
  server.registerTool("gog_chat_dm_space", {
@@ -34480,7 +35340,7 @@ function registerChatTools(server) {
34480
35340
  account: accountParam
34481
35341
  })
34482
35342
  }, async ({ email: email3, account }) => {
34483
- return runOrDiagnose(["chat", "dm", "space", email3], { account });
35343
+ return runOrDiagnose(["chat", "dm", "space", pos(email3)], { account });
34484
35344
  });
34485
35345
  server.registerTool("gog_chat_reactions_list", {
34486
35346
  description: "List the emoji reactions on one message, with who reacted. gog_chat_messages_list already returns a reaction SUMMARY per message; come here when you need the individual reactors, or the reaction resource names that gog_chat_reactions_delete takes." + workspaceOnlyNote,
@@ -34492,7 +35352,7 @@ function registerChatTools(server) {
34492
35352
  account: accountParam
34493
35353
  })
34494
35354
  }, async ({ message, space, max, pageToken, page, all, account }) => {
34495
- const args = ["chat", "messages", "reactions", "list", message];
35355
+ const args = ["chat", "messages", "reactions", "list", pos(message)];
34496
35356
  if (space) args.push(`--space=${space}`);
34497
35357
  pushPaginationFlags(args, { max, pageToken, page, all });
34498
35358
  return runOrDiagnose(args, { account });
@@ -34507,7 +35367,7 @@ function registerChatTools(server) {
34507
35367
  account: accountParam
34508
35368
  })
34509
35369
  }, async ({ message, emoji: emoji3, space, account }) => {
34510
- const args = ["chat", "messages", "reactions", "create", message, emoji3];
35370
+ const args = ["chat", "messages", "reactions", "create", pos(message), pos(emoji3)];
34511
35371
  if (space) args.push(`--space=${space}`);
34512
35372
  return runOrDiagnose(args, { account });
34513
35373
  });
@@ -34519,16 +35379,50 @@ function registerChatTools(server) {
34519
35379
  account: accountParam
34520
35380
  })
34521
35381
  }, async ({ reaction, account }) => {
34522
- return runOrDiagnose(["chat", "messages", "reactions", "delete", reaction], { account });
35382
+ return runOrDiagnose(["chat", "messages", "reactions", "delete", pos(reaction)], { account });
34523
35383
  });
34524
35384
  registerRunTool(server, {
34525
35385
  service: "chat",
34526
35386
  examples: '"spaces", "messages", "dm"',
35387
+ vet: vetChatRun,
34527
35388
  note: "Google Chat has no API for consumer accounts: every chat subcommand fails on an @gmail.com account regardless of scopes."
34528
35389
  });
34529
35390
  }
34530
35391
 
34531
35392
  // src/tools/classroom.ts
35393
+ var CLASSROOM_CREATE_WORDS = /* @__PURE__ */ new Set(["create", "add", "new"]);
35394
+ var CLASSROOM_GATED = {
35395
+ announcements: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
35396
+ announcement: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
35397
+ ann: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
35398
+ invitations: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
35399
+ invitation: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
35400
+ invites: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
35401
+ };
35402
+ function vetClassroomRun(subcommand, args) {
35403
+ const sub = subcommand.toLowerCase();
35404
+ const gated = Object.hasOwn(CLASSROOM_GATED, sub) ? CLASSROOM_GATED[sub] : void 0;
35405
+ if (!gated) return void 0;
35406
+ const word = hasCommandWord(args, CLASSROOM_CREATE_WORDS);
35407
+ return word ? gatedElsewhere(`gog classroom ${sub} ${word.toLowerCase()}`, "gog_classroom_run", gated.does, gated.tool) : void 0;
35408
+ }
35409
+ async function readCourse(courseId, account, runner = runOrDiagnose) {
35410
+ const got = await runner(["classroom", "courses", "get", pos(courseId)], { account });
35411
+ if (got.isError) return { error: got };
35412
+ let course;
35413
+ try {
35414
+ course = JSON.parse(resultText(got))?.course;
35415
+ } catch {
35416
+ course = void 0;
35417
+ }
35418
+ return {
35419
+ course: {
35420
+ id: courseId,
35421
+ ...typeof course?.name === "string" ? { name: course.name } : {},
35422
+ ...typeof course?.section === "string" ? { section: course.section } : {}
35423
+ }
35424
+ };
35425
+ }
34532
35426
  function registerClassroomTools(server) {
34533
35427
  server.registerTool("gog_classroom_courses_list", {
34534
35428
  description: "List Google Classroom courses.",
@@ -34562,7 +35456,7 @@ function registerClassroomTools(server) {
34562
35456
  account: accountParam
34563
35457
  })
34564
35458
  }, async ({ courseId, account }) => {
34565
- return runOrDiagnose(["classroom", "courses", "get", courseId], { account });
35459
+ return runOrDiagnose(["classroom", "courses", "get", pos(courseId)], { account });
34566
35460
  });
34567
35461
  server.registerTool("gog_classroom_students_list", {
34568
35462
  description: "List students enrolled in a Google Classroom course.",
@@ -34576,7 +35470,7 @@ function registerClassroomTools(server) {
34576
35470
  account: accountParam
34577
35471
  })
34578
35472
  }, async ({ courseId, max, pageToken, page, all, account }) => {
34579
- const args = ["classroom", "students", "list", courseId];
35473
+ const args = ["classroom", "students", "list", pos(courseId)];
34580
35474
  if (max !== void 0) args.push(`--max=${max}`);
34581
35475
  const token = resolvePageToken({ pageToken, page });
34582
35476
  if (token) args.push(`--page=${token}`);
@@ -34592,7 +35486,7 @@ function registerClassroomTools(server) {
34592
35486
  account: accountParam
34593
35487
  })
34594
35488
  }, async ({ courseId, userId, account }) => {
34595
- return runOrDiagnose(["classroom", "students", "get", courseId, userId], { account });
35489
+ return runOrDiagnose(["classroom", "students", "get", pos(courseId), pos(userId)], { account });
34596
35490
  });
34597
35491
  server.registerTool("gog_classroom_teachers_list", {
34598
35492
  description: "List teachers in a Google Classroom course.",
@@ -34606,7 +35500,7 @@ function registerClassroomTools(server) {
34606
35500
  account: accountParam
34607
35501
  })
34608
35502
  }, async ({ courseId, max, pageToken, page, all, account }) => {
34609
- const args = ["classroom", "teachers", "list", courseId];
35503
+ const args = ["classroom", "teachers", "list", pos(courseId)];
34610
35504
  if (max !== void 0) args.push(`--max=${max}`);
34611
35505
  const token = resolvePageToken({ pageToken, page });
34612
35506
  if (token) args.push(`--page=${token}`);
@@ -34622,7 +35516,7 @@ function registerClassroomTools(server) {
34622
35516
  account: accountParam
34623
35517
  })
34624
35518
  }, async ({ courseId, userId, account }) => {
34625
- return runOrDiagnose(["classroom", "teachers", "get", courseId, userId], { account });
35519
+ return runOrDiagnose(["classroom", "teachers", "get", pos(courseId), pos(userId)], { account });
34626
35520
  });
34627
35521
  server.registerTool("gog_classroom_roster", {
34628
35522
  description: "List the full roster (students and/or teachers) of a Google Classroom course. Omit both flags to return both groups.",
@@ -34638,7 +35532,7 @@ function registerClassroomTools(server) {
34638
35532
  account: accountParam
34639
35533
  })
34640
35534
  }, async ({ courseId, students, teachers, max, pageToken, page, all, account }) => {
34641
- const args = ["classroom", "roster", courseId];
35535
+ const args = ["classroom", "roster", pos(courseId)];
34642
35536
  if (students) args.push("--students");
34643
35537
  if (teachers) args.push("--teachers");
34644
35538
  if (max !== void 0) args.push(`--max=${max}`);
@@ -34663,7 +35557,7 @@ function registerClassroomTools(server) {
34663
35557
  account: accountParam
34664
35558
  })
34665
35559
  }, async ({ courseId, state, topic, orderBy, max, pageToken, page, all, scanPages, account }) => {
34666
- const args = ["classroom", "coursework", "list", courseId];
35560
+ const args = ["classroom", "coursework", "list", pos(courseId)];
34667
35561
  if (state) args.push(`--state=${state}`);
34668
35562
  if (topic) args.push(`--topic=${topic}`);
34669
35563
  if (orderBy) args.push(`--order-by=${orderBy}`);
@@ -34683,7 +35577,7 @@ function registerClassroomTools(server) {
34683
35577
  account: accountParam
34684
35578
  })
34685
35579
  }, async ({ courseId, courseworkId, account }) => {
34686
- return runOrDiagnose(["classroom", "coursework", "get", courseId, courseworkId], { account });
35580
+ return runOrDiagnose(["classroom", "coursework", "get", pos(courseId), pos(courseworkId)], { account });
34687
35581
  });
34688
35582
  server.registerTool("gog_classroom_submissions_list", {
34689
35583
  description: "List student submissions for a coursework item.",
@@ -34701,7 +35595,7 @@ function registerClassroomTools(server) {
34701
35595
  account: accountParam
34702
35596
  })
34703
35597
  }, async ({ courseId, courseworkId, state, late, user, max, pageToken, page, all, account }) => {
34704
- const args = ["classroom", "submissions", "list", courseId, courseworkId];
35598
+ const args = ["classroom", "submissions", "list", pos(courseId), pos(courseworkId)];
34705
35599
  if (state) args.push(`--state=${state}`);
34706
35600
  if (late) args.push(`--late=${late}`);
34707
35601
  if (user) args.push(`--user=${user}`);
@@ -34721,7 +35615,7 @@ function registerClassroomTools(server) {
34721
35615
  account: accountParam
34722
35616
  })
34723
35617
  }, async ({ courseId, courseworkId, submissionId, account }) => {
34724
- return runOrDiagnose(["classroom", "submissions", "get", courseId, courseworkId, submissionId], { account });
35618
+ return runOrDiagnose(["classroom", "submissions", "get", pos(courseId), pos(courseworkId), pos(submissionId)], { account });
34725
35619
  });
34726
35620
  server.registerTool("gog_classroom_submissions_grade", {
34727
35621
  description: "Grade a student submission. Set draft and/or assigned grade values.",
@@ -34735,7 +35629,7 @@ function registerClassroomTools(server) {
34735
35629
  account: accountParam
34736
35630
  })
34737
35631
  }, async ({ courseId, courseworkId, submissionId, draft, assigned, account }) => {
34738
- const args = ["classroom", "submissions", "grade", courseId, courseworkId, submissionId];
35632
+ const args = ["classroom", "submissions", "grade", pos(courseId), pos(courseworkId), pos(submissionId)];
34739
35633
  if (draft) args.push(`--draft=${draft}`);
34740
35634
  if (assigned) args.push(`--assigned=${assigned}`);
34741
35635
  return runOrDiagnose(args, { account });
@@ -34750,7 +35644,7 @@ function registerClassroomTools(server) {
34750
35644
  account: accountParam
34751
35645
  })
34752
35646
  }, async ({ courseId, courseworkId, submissionId, account }) => {
34753
- return runOrDiagnose(["classroom", "submissions", "return", courseId, courseworkId, submissionId], { account });
35647
+ return runOrDiagnose(["classroom", "submissions", "return", pos(courseId), pos(courseworkId), pos(submissionId)], { account });
34754
35648
  });
34755
35649
  server.registerTool("gog_classroom_submissions_turn_in", {
34756
35650
  description: "Turn in a student submission (student action).",
@@ -34762,7 +35656,7 @@ function registerClassroomTools(server) {
34762
35656
  account: accountParam
34763
35657
  })
34764
35658
  }, async ({ courseId, courseworkId, submissionId, account }) => {
34765
- return runOrDiagnose(["classroom", "submissions", "turn-in", courseId, courseworkId, submissionId], { account });
35659
+ return runOrDiagnose(["classroom", "submissions", "turn-in", pos(courseId), pos(courseworkId), pos(submissionId)], { account });
34766
35660
  });
34767
35661
  server.registerTool("gog_classroom_submissions_reclaim", {
34768
35662
  description: "Reclaim a turned-in submission (student action to edit a submission).",
@@ -34774,7 +35668,7 @@ function registerClassroomTools(server) {
34774
35668
  account: accountParam
34775
35669
  })
34776
35670
  }, async ({ courseId, courseworkId, submissionId, account }) => {
34777
- return runOrDiagnose(["classroom", "submissions", "reclaim", courseId, courseworkId, submissionId], { account });
35671
+ return runOrDiagnose(["classroom", "submissions", "reclaim", pos(courseId), pos(courseworkId), pos(submissionId)], { account });
34778
35672
  });
34779
35673
  server.registerTool("gog_classroom_announcements_list", {
34780
35674
  description: "List announcements in a Google Classroom course.",
@@ -34790,7 +35684,7 @@ function registerClassroomTools(server) {
34790
35684
  account: accountParam
34791
35685
  })
34792
35686
  }, async ({ courseId, state, orderBy, max, pageToken, page, all, account }) => {
34793
- const args = ["classroom", "announcements", "list", courseId];
35687
+ const args = ["classroom", "announcements", "list", pos(courseId)];
34794
35688
  if (state) args.push(`--state=${state}`);
34795
35689
  if (orderBy) args.push(`--order-by=${orderBy}`);
34796
35690
  if (max !== void 0) args.push(`--max=${max}`);
@@ -34808,22 +35702,45 @@ function registerClassroomTools(server) {
34808
35702
  account: accountParam
34809
35703
  })
34810
35704
  }, async ({ courseId, announcementId, account }) => {
34811
- return runOrDiagnose(["classroom", "announcements", "get", courseId, announcementId], { account });
35705
+ return runOrDiagnose(["classroom", "announcements", "get", pos(courseId), pos(announcementId)], { account });
34812
35706
  });
34813
35707
  server.registerTool("gog_classroom_announcements_create", {
34814
- description: "Create an announcement in a Google Classroom course.",
35708
+ 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,
34815
35709
  annotations: { destructiveHint: true },
34816
35710
  inputSchema: external_exports.object({
34817
35711
  courseId: external_exports.string().describe("Course ID"),
34818
35712
  text: external_exports.string().describe("Announcement text"),
34819
- state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State"),
35713
+ state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State (DRAFT is visible only to teachers and needs no confirmation)"),
34820
35714
  scheduled: external_exports.string().optional().describe("Scheduled publish time"),
34821
- account: accountParam
35715
+ account: accountParam,
35716
+ confirmToken: confirmTokenParam
34822
35717
  })
34823
- }, async ({ courseId, text, state, scheduled, account }) => {
34824
- const args = ["classroom", "announcements", "create", courseId, `--text=${text}`];
35718
+ }, async ({ courseId, text, state, scheduled, account, confirmToken }, ctx) => {
35719
+ const args = ["classroom", "announcements", "create", pos(courseId), `--text=${text}`];
34825
35720
  if (state) args.push(`--state=${state}`);
34826
35721
  if (scheduled) args.push(`--scheduled=${scheduled}`);
35722
+ if (state !== "DRAFT") {
35723
+ const read = await readCourse(courseId, account);
35724
+ if (read.error) return read.error;
35725
+ const publishes = scheduled ? `at ${scheduled}` : "immediately";
35726
+ const confirmation = await requireDispatchConfirmation(ctx, {
35727
+ action: "classroom.announcement-create",
35728
+ message: "Review and confirm this Classroom announcement:",
35729
+ confirmationLabel: "Confirm that this announcement should be posted to the class.",
35730
+ details: { course: read.course, publishes, textPreview: bodyPreview(text) },
35731
+ unsupportedNote: "Create it with state DRAFT instead; the user can review and post it from Classroom.",
35732
+ fallback: {
35733
+ tool: "gog_classroom_announcements_create",
35734
+ account,
35735
+ confirmToken,
35736
+ subject: () => {
35737
+ const view = { course: read.course, publishes, text };
35738
+ return { target: courseId, payload: view, preview: view };
35739
+ }
35740
+ }
35741
+ });
35742
+ if (confirmation) return confirmation;
35743
+ }
34827
35744
  return runOrDiagnose(args, { account });
34828
35745
  });
34829
35746
  server.registerTool("gog_classroom_topics_list", {
@@ -34838,7 +35755,7 @@ function registerClassroomTools(server) {
34838
35755
  account: accountParam
34839
35756
  })
34840
35757
  }, async ({ courseId, max, pageToken, page, all, account }) => {
34841
- const args = ["classroom", "topics", "list", courseId];
35758
+ const args = ["classroom", "topics", "list", pos(courseId)];
34842
35759
  if (max !== void 0) args.push(`--max=${max}`);
34843
35760
  const token = resolvePageToken({ pageToken, page });
34844
35761
  if (token) args.push(`--page=${token}`);
@@ -34854,7 +35771,7 @@ function registerClassroomTools(server) {
34854
35771
  account: accountParam
34855
35772
  })
34856
35773
  }, async ({ courseId, topicId, account }) => {
34857
- return runOrDiagnose(["classroom", "topics", "get", courseId, topicId], { account });
35774
+ return runOrDiagnose(["classroom", "topics", "get", pos(courseId), pos(topicId)], { account });
34858
35775
  });
34859
35776
  server.registerTool("gog_classroom_invitations_list", {
34860
35777
  description: "List Google Classroom invitations.",
@@ -34886,7 +35803,7 @@ function registerClassroomTools(server) {
34886
35803
  account: accountParam
34887
35804
  })
34888
35805
  }, async ({ invitationId, account }) => {
34889
- return runOrDiagnose(["classroom", "invitations", "get", invitationId], { account });
35806
+ return runOrDiagnose(["classroom", "invitations", "get", pos(invitationId)], { account });
34890
35807
  });
34891
35808
  server.registerTool("gog_classroom_invitations_accept", {
34892
35809
  description: "Accept a Google Classroom invitation.",
@@ -34896,7 +35813,7 @@ function registerClassroomTools(server) {
34896
35813
  account: accountParam
34897
35814
  })
34898
35815
  }, async ({ invitationId, account }) => {
34899
- return runOrDiagnose(["classroom", "invitations", "accept", invitationId], { account });
35816
+ return runOrDiagnose(["classroom", "invitations", "accept", pos(invitationId)], { account });
34900
35817
  });
34901
35818
  server.registerTool("gog_classroom_profile_get", {
34902
35819
  description: "Get a Google Classroom user profile. Omit userId to fetch the authenticated user.",
@@ -34907,12 +35824,13 @@ function registerClassroomTools(server) {
34907
35824
  })
34908
35825
  }, async ({ userId, account }) => {
34909
35826
  const args = ["classroom", "profile", "get"];
34910
- if (userId) args.push(userId);
35827
+ if (userId) args.push(pos(userId));
34911
35828
  return runOrDiagnose(args, { account });
34912
35829
  });
34913
35830
  registerRunTool(server, {
34914
35831
  service: "classroom",
34915
35832
  examples: '"guardians", "materials", "guardian-invitations"',
35833
+ vet: vetClassroomRun,
34916
35834
  note: "Covers anything not wrapped by the dedicated tools (guardians, guardian-invitations, materials, coursework assignees, announcement assignees, etc.)."
34917
35835
  });
34918
35836
  }
@@ -34927,7 +35845,7 @@ function registerContactsTools(server) {
34927
35845
  account: accountParam
34928
35846
  })
34929
35847
  }, async ({ query, account }) => {
34930
- return runOrDiagnose(["contacts", "search", query], { account });
35848
+ return runOrDiagnose(["contacts", "search", pos(query)], { account });
34931
35849
  });
34932
35850
  server.registerTool("gog_contacts_list", {
34933
35851
  description: "List all Google Contacts.",
@@ -34946,7 +35864,7 @@ function registerContactsTools(server) {
34946
35864
  account: accountParam
34947
35865
  })
34948
35866
  }, async ({ resourceName, account }) => {
34949
- return runOrDiagnose(["contacts", "get", resourceName], { account });
35867
+ return runOrDiagnose(["contacts", "get", pos(resourceName)], { account });
34950
35868
  });
34951
35869
  server.registerTool("gog_contacts_create", {
34952
35870
  description: "Create a new Google Contact.",
@@ -34982,7 +35900,7 @@ function registerDocsTools(server) {
34982
35900
  account: accountParam
34983
35901
  })
34984
35902
  }, async ({ docId, account }) => {
34985
- return runOrDiagnose(["docs", "info", docId], { account });
35903
+ return runOrDiagnose(["docs", "info", pos(docId)], { account });
34986
35904
  });
34987
35905
  server.registerTool("gog_docs_cat", {
34988
35906
  description: "Read a Google Doc as plain text.",
@@ -34993,7 +35911,7 @@ function registerDocsTools(server) {
34993
35911
  account: accountParam
34994
35912
  })
34995
35913
  }, async ({ docId, chips, account }) => {
34996
- const args = ["docs", "cat", docId];
35914
+ const args = ["docs", "cat", pos(docId)];
34997
35915
  if (chips) args.push("--chips");
34998
35916
  return runOrDiagnose(args, { account });
34999
35917
  });
@@ -35005,7 +35923,7 @@ function registerDocsTools(server) {
35005
35923
  account: accountParam
35006
35924
  })
35007
35925
  }, async ({ title, account }) => {
35008
- return runOrDiagnose(["docs", "create", title], { account });
35926
+ return runOrDiagnose(["docs", "create", pos(title)], { account });
35009
35927
  });
35010
35928
  server.registerTool("gog_docs_write", {
35011
35929
  description: "Write text content to a Google Doc, replacing existing body content by default. Set append=true to add after existing content.",
@@ -35030,7 +35948,7 @@ function registerDocsTools(server) {
35030
35948
  account: accountParam
35031
35949
  })
35032
35950
  }, async ({ docId, text, append, checkOrphans, bullets, bulletPreset, ordered, noBullets, indentStart, indentEnd, indentFirstLine, spaceAbove, spaceBelow, keepLinesTogether, keepWithNext, batch, account }) => {
35033
- const args = ["docs", "write", docId, `--text=${text}`];
35951
+ const args = ["docs", "write", pos(docId), `--text=${text}`];
35034
35952
  if (append) args.push("--append");
35035
35953
  if (checkOrphans) args.push("--check-orphans");
35036
35954
  if (bullets) args.push("--bullets");
@@ -35057,7 +35975,7 @@ function registerDocsTools(server) {
35057
35975
  account: accountParam
35058
35976
  })
35059
35977
  }, async ({ docId, find, replace, account }) => {
35060
- return runOrDiagnose(["docs", "find-replace", docId, find, replace], { account });
35978
+ return runOrDiagnose(["docs", "find-replace", pos(docId), pos(find), pos(replace)], { account });
35061
35979
  });
35062
35980
  server.registerTool("gog_docs_structure", {
35063
35981
  description: "Show a Google Doc's structure with numbered paragraphs. Useful for understanding the document layout before making index-based edits.",
@@ -35067,18 +35985,31 @@ function registerDocsTools(server) {
35067
35985
  account: accountParam
35068
35986
  })
35069
35987
  }, async ({ docId, account }) => {
35070
- return runOrDiagnose(["docs", "structure", docId], { account });
35988
+ return runOrDiagnose(["docs", "structure", pos(docId)], { account });
35071
35989
  });
35072
35990
  registerRunTool(server, { service: "docs", examples: '"copy", "clear", "insert", "sed", "export"' });
35073
35991
  }
35074
35992
 
35075
35993
  // src/tools/drive.ts
35994
+ function vetDriveRun(subcommand, _args) {
35995
+ return subcommand.toLowerCase() === "share" ? gatedElsewhere("gog drive share", "gog_drive_run", "grants access to a file", "gog_drive_share") : void 0;
35996
+ }
35076
35997
  var GOOGLE_DOC_MIME = "application/vnd.google-apps.document";
35998
+ function shareTargetMeta(raw) {
35999
+ try {
36000
+ const { name, mimeType } = fileMeta(raw);
36001
+ return { name, mimeType };
36002
+ } catch {
36003
+ return {};
36004
+ }
36005
+ }
35077
36006
  function fileMeta(raw) {
35078
36007
  const parsed = JSON.parse(raw);
35079
36008
  const f = parsed.file ?? parsed;
35080
- return { name: f.name, mimeType: f.mimeType };
36009
+ const size = f.size === void 0 ? void 0 : Number(f.size);
36010
+ return { name: f.name, mimeType: f.mimeType, size: Number.isFinite(size) ? size : void 0 };
35081
36011
  }
36012
+ var MAX_DRIVE_READ_BYTES = MAX_INLINE_ATTACHMENT_BYTES;
35082
36013
  var DRIVE_LS_COMPACT_FIELDS = "nextPageToken,files(id,name,mimeType,modifiedTime,size,webViewLink)";
35083
36014
  function registerDriveTools(server) {
35084
36015
  server.registerTool("gog_drive_ls", {
@@ -35122,7 +36053,7 @@ function registerDriveTools(server) {
35122
36053
  account: accountParam
35123
36054
  })
35124
36055
  }, async ({ query, view, account }) => {
35125
- return runOrDiagnose(["drive", "search", query], {
36056
+ return runOrDiagnose(["drive", "search", pos(query)], {
35126
36057
  account,
35127
36058
  stripMedia: resolveView(view, ["compact", "full"]) === "compact"
35128
36059
  });
@@ -35142,7 +36073,7 @@ function registerDriveTools(server) {
35142
36073
  account: accountParam
35143
36074
  })
35144
36075
  }, async ({ fileId, view, account }) => {
35145
- return runOrDiagnose(["drive", "get", fileId], {
36076
+ return runOrDiagnose(["drive", "get", pos(fileId)], {
35146
36077
  account,
35147
36078
  stripMedia: resolveView(view, ["compact", "full"]) === "compact"
35148
36079
  });
@@ -35155,7 +36086,7 @@ function registerDriveTools(server) {
35155
36086
  account: accountParam
35156
36087
  })
35157
36088
  }, async ({ name, account }) => {
35158
- return runOrDiagnose(["drive", "mkdir", name], { account });
36089
+ return runOrDiagnose(["drive", "mkdir", pos(name)], { account });
35159
36090
  });
35160
36091
  server.registerTool("gog_drive_rename", {
35161
36092
  description: "Rename a file or folder in Google Drive.",
@@ -35166,7 +36097,7 @@ function registerDriveTools(server) {
35166
36097
  account: accountParam
35167
36098
  })
35168
36099
  }, async ({ fileId, newName, account }) => {
35169
- return runOrDiagnose(["drive", "rename", fileId, newName], { account });
36100
+ return runOrDiagnose(["drive", "rename", pos(fileId), pos(newName)], { account });
35170
36101
  });
35171
36102
  server.registerTool("gog_drive_move", {
35172
36103
  description: "Move a file to a different folder in Google Drive.",
@@ -35177,7 +36108,7 @@ function registerDriveTools(server) {
35177
36108
  account: accountParam
35178
36109
  })
35179
36110
  }, async ({ fileId, parentId, account }) => {
35180
- return runOrDiagnose(["drive", "move", fileId, `--parent=${parentId}`], { account });
36111
+ return runOrDiagnose(["drive", "move", pos(fileId), `--parent=${parentId}`], { account });
35181
36112
  });
35182
36113
  server.registerTool("gog_drive_delete", {
35183
36114
  description: "Move a Google Drive file to trash, or permanently delete it with permanent=true (irreversible).",
@@ -35188,13 +36119,13 @@ function registerDriveTools(server) {
35188
36119
  account: accountParam
35189
36120
  })
35190
36121
  }, async ({ fileId, permanent, account }) => {
35191
- const args = ["drive", "delete", fileId];
36122
+ const args = ["drive", "delete", pos(fileId)];
35192
36123
  if (permanent) args.push("--permanent");
35193
36124
  args.push("--force");
35194
36125
  return runOrDiagnose(args, { account });
35195
36126
  });
35196
36127
  server.registerTool("gog_drive_share", {
35197
- description: "Share a Google Drive file or folder.",
36128
+ 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,
35198
36129
  annotations: { destructiveHint: true },
35199
36130
  inputSchema: external_exports.object({
35200
36131
  fileId: external_exports.string().describe("File or folder ID"),
@@ -35202,10 +36133,36 @@ function registerDriveTools(server) {
35202
36133
  email: external_exports.string().optional().describe("User email (required when to=user)"),
35203
36134
  domain: external_exports.string().optional().describe("Domain (required when to=domain)"),
35204
36135
  role: external_exports.enum(["reader", "writer"]).optional().describe("Permission role (default: reader)"),
35205
- account: accountParam
36136
+ account: accountParam,
36137
+ confirmToken: confirmTokenParam
35206
36138
  })
35207
- }, async ({ fileId, to, email: email3, domain: domain2, role, account }) => {
35208
- const args = ["drive", "share", fileId, `--to=${to}`];
36139
+ }, async ({ fileId, to, email: email3, domain: domain2, role, account, confirmToken }, ctx) => {
36140
+ const got = await runOrDiagnose(["drive", "get", pos(fileId)], { account });
36141
+ if (got.isError) return got;
36142
+ const file2 = { id: fileId, ...shareTargetMeta(resultText(got)) };
36143
+ const grant = {
36144
+ file: file2,
36145
+ to,
36146
+ ...email3 ? { email: email3 } : {},
36147
+ ...domain2 ? { domain: domain2 } : {},
36148
+ role: role ?? "reader",
36149
+ publicLink: to === "anyone"
36150
+ };
36151
+ const confirmation = await requireDispatchConfirmation(ctx, {
36152
+ action: "drive.share",
36153
+ message: to === "anyone" ? "Review and confirm making this file available to ANYONE with the link:" : "Review and confirm this Drive share:",
36154
+ confirmationLabel: "Confirm that this access should be granted now.",
36155
+ details: grant,
36156
+ unsupportedNote: "Ask the user to share it themselves from Google Drive.",
36157
+ fallback: {
36158
+ tool: "gog_drive_share",
36159
+ account,
36160
+ confirmToken,
36161
+ subject: () => ({ target: fileId, payload: grant, preview: grant })
36162
+ }
36163
+ });
36164
+ if (confirmation) return confirmation;
36165
+ const args = ["drive", "share", pos(fileId), `--to=${to}`];
35209
36166
  if (email3) args.push(`--email=${email3}`);
35210
36167
  if (domain2) args.push(`--domain=${domain2}`);
35211
36168
  if (role) args.push(`--role=${role}`);
@@ -35228,7 +36185,7 @@ function registerDriveTools(server) {
35228
36185
  }, async ({ fileId, ocrLanguage, offset = 0, maxChars, account }) => {
35229
36186
  let tempDocId;
35230
36187
  try {
35231
- const { name, mimeType } = fileMeta(await run(["drive", "get", fileId], { account }));
36188
+ const { name, mimeType } = fileMeta(await run(["drive", "get", pos(fileId)], { account }));
35232
36189
  let sourceId = fileId;
35233
36190
  if (mimeType !== GOOGLE_DOC_MIME) {
35234
36191
  const params = JSON.stringify({ fileId, ...ocrLanguage ? { ocrLanguage } : {} });
@@ -35273,13 +36230,13 @@ function registerDriveTools(server) {
35273
36230
  return diagnose(err);
35274
36231
  } finally {
35275
36232
  if (tempDocId) {
35276
- await run(["drive", "delete", tempDocId, "--permanent", "--force"], { account }).catch(() => {
36233
+ await run(["drive", "delete", pos(tempDocId), "--permanent", "--force"], { account }).catch(() => {
35277
36234
  });
35278
36235
  }
35279
36236
  }
35280
36237
  });
35281
36238
  server.registerTool("gog_drive_read_bytes", {
35282
- description: "Fetch a Drive file's raw bytes and return them base64-encoded as an embedded resource \u2014 the generic fallback for callers that want the file itself (to parse locally) rather than extracted text. For readable text from a PDF, prefer gog_drive_extract_text.",
36239
+ description: "Fetch a Drive file's raw bytes and return them base64-encoded as an embedded resource \u2014 the generic fallback for callers that want the file itself (to parse locally) rather than extracted text. Files over 8 MiB are refused (checked before downloading). For readable text from a PDF, prefer gog_drive_extract_text.",
35283
36240
  annotations: { readOnlyHint: true },
35284
36241
  inputSchema: external_exports.object({
35285
36242
  fileId: external_exports.string().describe("Drive file ID"),
@@ -35287,9 +36244,17 @@ function registerDriveTools(server) {
35287
36244
  })
35288
36245
  }, async ({ fileId, account }) => {
35289
36246
  try {
35290
- const { name, mimeType } = fileMeta(await run(["drive", "get", fileId], { account }));
36247
+ const { name, mimeType, size } = fileMeta(await run(["drive", "get", pos(fileId)], { account }));
36248
+ if (size !== void 0 && size > MAX_DRIVE_READ_BYTES) {
36249
+ return errorResult(
36250
+ `${name ?? fileId} is ${size} bytes, over the 8 MiB this tool returns inline. Use gog_drive_extract_text for its text, or share its webViewLink (gog_drive_get) instead.`
36251
+ );
36252
+ }
35291
36253
  const params = JSON.stringify({ fileId, alt: "media" });
35292
- const blob = await runBinary(["api", "call", "drive", "v3", "files.get", `--params=${params}`], { account });
36254
+ const blob = await runBinary(
36255
+ ["api", "call", "drive", "v3", "files.get", `--params=${params}`],
36256
+ { account, maxOutputBytes: MAX_DRIVE_READ_BYTES }
36257
+ );
35293
36258
  const type = mimeType ?? "application/octet-stream";
35294
36259
  const unrenderableHint = type.startsWith("image/") ? "" : ` If your client does not render a ${type} resource, gog_drive_extract_text returns the same file as text (PDFs included, via OCR).`;
35295
36260
  return {
@@ -35309,7 +36274,7 @@ function registerDriveTools(server) {
35309
36274
  return diagnose(err);
35310
36275
  }
35311
36276
  });
35312
- registerRunTool(server, { service: "drive", examples: '"copy", "upload", "download", "permissions"' });
36277
+ registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"', vet: vetDriveRun });
35313
36278
  }
35314
36279
 
35315
36280
  // src/gmail-results.ts
@@ -35337,7 +36302,7 @@ async function countMatches(method, itemsKey, query, account) {
35337
36302
  maxResults: COUNT_PROBE_PAGE_SIZE,
35338
36303
  fields: `${itemsKey}/id,nextPageToken`
35339
36304
  });
35340
- const raw = await run(["api", "call", "gmail", "v1", method, `--params=${params}`], { account });
36305
+ const raw = await run(["api", "call", "gmail", "v1", pos(method), `--params=${params}`], { account });
35341
36306
  const parsed = JSON.parse(raw);
35342
36307
  const items = parsed[itemsKey];
35343
36308
  if (!Array.isArray(items)) return {};
@@ -35376,7 +36341,7 @@ async function fetchGmailPages(runPage, itemsKey, maxPages, startToken) {
35376
36341
  const result = await runPage(token);
35377
36342
  const parsed = parsePage(result, itemsKey);
35378
36343
  if (parsed === void 0) {
35379
- return base === void 0 ? result : finish(base, itemsKey, merged, token);
36344
+ return base === void 0 ? result : finish(base, itemsKey, merged, token, pageErrorText(result, itemsKey, pages + 1));
35380
36345
  }
35381
36346
  base = parsed;
35382
36347
  merged.push(...parsed[itemsKey]);
@@ -35407,10 +36372,18 @@ function parsePage(result, itemsKey) {
35407
36372
  const obj = parsed;
35408
36373
  return Array.isArray(obj[itemsKey]) ? obj : void 0;
35409
36374
  }
35410
- function finish(base, itemsKey, merged, token) {
36375
+ var PAGE_ERROR_MAX = 2e3;
36376
+ function pageErrorText(result, itemsKey, n) {
36377
+ const first = result.content[0];
36378
+ const text = first?.type === "text" ? first.text.slice(0, PAGE_ERROR_MAX) : "";
36379
+ if (result.isError) return `page ${n} failed${text ? `: ${text}` : ""}`;
36380
+ return `page ${n} returned output that is not a ${itemsKey} list`;
36381
+ }
36382
+ function finish(base, itemsKey, merged, token, pageError) {
35411
36383
  const out = { ...base, [itemsKey]: merged };
35412
36384
  if (token === void 0) delete out.nextPageToken;
35413
36385
  else out.nextPageToken = token;
36386
+ if (pageError !== void 0) out.pageError = pageError;
35414
36387
  return rawTextResult(JSON.stringify(out));
35415
36388
  }
35416
36389
 
@@ -35424,16 +36397,19 @@ var STAGING_TWIN = {
35424
36397
  "gmail.reply-all": "gog_gmail_drafts_reply_all",
35425
36398
  "gmail.send": "gog_gmail_drafts_create"
35426
36399
  };
35427
- function requireGmailDispatchConfirmation(ctx, op, details) {
36400
+ var UNSUPPORTED_NOTE2 = {
36401
+ "gmail.drafts-send": "The draft is still saved: ask the user to review it and send it from Gmail."
36402
+ };
36403
+ async function requireGmailDispatchConfirmation(ctx, op, details, fallback) {
35428
36404
  const twin = STAGING_TWIN[op];
35429
- return requireConfirmation(ctx, {
36405
+ 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];
36406
+ return requireDispatchConfirmation(ctx, {
35430
36407
  action: op,
35431
- message: "Review and confirm this email dispatch:",
36408
+ message: op === "gmail.filter-forward" ? "Review and confirm this mail-forwarding filter:" : "Review and confirm this email dispatch:",
35432
36409
  details,
35433
- confirmationLabel: "Confirm that this email should be sent now.",
35434
- ...twin ? {
35435
- unsupportedNote: `Stage it with ${twin} instead, review the draft, and send it with gog_gmail_drafts_send.`
35436
- } : {}
36410
+ confirmationLabel: op === "gmail.filter-forward" ? "Confirm that matching mail should be forwarded automatically from now on." : "Confirm that this email should be sent now.",
36411
+ unsupportedNote: note,
36412
+ ...fallback ? { fallback: { instruction: CONFIRM_SEND_INSTRUCTION, ...fallback } } : {}
35437
36413
  });
35438
36414
  }
35439
36415
  var EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/gi;
@@ -35487,10 +36463,6 @@ function logGmailDispatch(tool, recipients, account) {
35487
36463
  process.stderr.write(`${JSON.stringify(event)}
35488
36464
  `);
35489
36465
  }
35490
- function resultText(result) {
35491
- const first = result.content[0];
35492
- return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
35493
- }
35494
36466
 
35495
36467
  // src/tools/gmail.ts
35496
36468
  var replySchema = {
@@ -35504,7 +36476,7 @@ var replySchema = {
35504
36476
  remove: external_exports.array(external_exports.string()).optional().describe("Remove these recipients from all fields (repeatable) \u2014 e.g. to drop someone from a reply-all."),
35505
36477
  subject: external_exports.string().optional().describe('Override reply subject (default: "Re: <original>"). A changed subject starts a NEW Gmail thread.'),
35506
36478
  noQuote: external_exports.boolean().optional().describe("Do not include the original message quoted below the reply (default: the original is quoted)"),
35507
- 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. Read on the server, base64-encoded with a MIME type inferred from the extension.`),
36479
+ 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. Read on the server, base64-encoded with a MIME type inferred from the extension. Must be inside the server's GOG_FILE_ROOTS directories (default ~/gogcli-mcp-files).`),
35508
36480
  attachInline: attachInlineParam,
35509
36481
  from: external_exports.string().optional().describe("Send from this email address (must be a verified send-as alias)"),
35510
36482
  autoFromAddressedAlias: external_exports.boolean().optional().describe("When from is omitted, send from the verified send-as alias the original message was addressed TO, instead of the account's primary address \u2014 so a reply to mail sent to an alias goes back out from that alias. Ignored when from is set."),
@@ -35513,8 +36485,14 @@ var replySchema = {
35513
36485
  signatureFile: external_exports.string().optional().describe("Append a local signature file (plain text or HTML), read on the gog server"),
35514
36486
  account: accountParam
35515
36487
  };
36488
+ function confineReplyPaths(f) {
36489
+ confinePaths(f.attach, "attach");
36490
+ if (f.bodyHtmlFile) confinePath(f.bodyHtmlFile, "bodyHtmlFile");
36491
+ if (f.signatureFile) confinePath(f.signatureFile, "signatureFile");
36492
+ }
35516
36493
  function appendReplyFlags(args, f) {
35517
36494
  assertNotBoth("bodyHtml", "bodyHtmlFile", f.bodyHtml, f.bodyHtmlFile);
36495
+ confineReplyPaths(f);
35518
36496
  if (f.body) args.push(payloadArg("body", "body-file", f.body));
35519
36497
  if (f.bodyHtml) args.push(payloadArg("body-html", "body-html-file", f.bodyHtml, "html"));
35520
36498
  else if (f.bodyHtmlFile) args.push(`--body-html-file=${f.bodyHtmlFile}`);
@@ -35532,7 +36510,7 @@ function appendReplyFlags(args, f) {
35532
36510
  if (f.signatureFile) args.push(`--signature-file=${f.signatureFile}`);
35533
36511
  args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
35534
36512
  }
35535
- var sendReplySchema = external_exports.object(replySchema);
36513
+ var sendReplySchema = external_exports.object({ ...replySchema, confirmToken: confirmTokenParam });
35536
36514
  function parseMetadataHeaders(raw) {
35537
36515
  let parsed;
35538
36516
  try {
@@ -35557,27 +36535,110 @@ function computeReplyRecipients(kind, headers, flags) {
35557
36535
  }
35558
36536
  return emails;
35559
36537
  }
35560
- async function sendReply(kind, toolName, messageId, account, flags, ctx) {
35561
- const metaResult = await runOrDiagnose(["gmail", "get", messageId, "--format=metadata"], { account });
36538
+ function metadataThreadId(raw) {
36539
+ try {
36540
+ const threadId = JSON.parse(raw)?.message?.threadId;
36541
+ return typeof threadId === "string" ? threadId : void 0;
36542
+ } catch {
36543
+ return void 0;
36544
+ }
36545
+ }
36546
+ function replyTokenSubject(kind, messageId, account, headers, threadId, flags) {
36547
+ const removed = new Set(extractEmails(...flags.remove ?? []));
36548
+ const keep = (emails) => emails.filter((e) => !removed.has(e));
36549
+ const to = keep(extractEmails(headers.from, ...kind === "reply-all" ? [headers.to] : [], ...flags.to ?? []));
36550
+ const cc = keep(extractEmails(...kind === "reply-all" ? [headers.cc] : [], ...flags.cc ?? []));
36551
+ const bcc = keep(extractEmails(...flags.bcc ?? []));
36552
+ const attachments = attachmentDetails(flags.attach, flags.attachInline);
36553
+ const subject = flags.subject || (headers.subject ? `Re: ${headers.subject}` : void 0);
36554
+ const from = flags.from ?? (flags.autoFromAddressedAlias ? "the send-as alias the original was addressed to" : senderPreview(account));
36555
+ const localFiles = attachmentDetails([flags.bodyHtmlFile, flags.signatureFile].filter((p) => Boolean(p)), void 0);
36556
+ return {
36557
+ target: messageId,
36558
+ payload: {
36559
+ kind,
36560
+ to,
36561
+ cc,
36562
+ bcc,
36563
+ subject,
36564
+ from,
36565
+ body: flags.body,
36566
+ bodyHtml: flags.bodyHtml,
36567
+ localFiles,
36568
+ signature: Boolean(flags.signature),
36569
+ signatureFrom: flags.signatureFrom,
36570
+ attachments,
36571
+ quote: !flags.noQuote,
36572
+ threadId,
36573
+ inReplyTo: headers.message_id,
36574
+ references: headers.references
36575
+ },
36576
+ preview: {
36577
+ from,
36578
+ to,
36579
+ cc,
36580
+ bcc,
36581
+ subject,
36582
+ body: flags.body,
36583
+ ...flags.bodyHtml ? { bodyHtml: flags.bodyHtml } : {},
36584
+ ...flags.bodyHtmlFile ? { bodyHtmlFile: flags.bodyHtmlFile } : {},
36585
+ ...flags.signature || flags.signatureFrom || flags.signatureFile ? { signature: flags.signatureFile ?? flags.signatureFrom ?? "the Gmail signature of the sending address" } : {},
36586
+ quotesOriginal: !flags.noQuote,
36587
+ attachments: attachmentPreview(attachments),
36588
+ replyingToMessageId: messageId,
36589
+ threadId,
36590
+ inReplyTo: headers.message_id
36591
+ }
36592
+ };
36593
+ }
36594
+ async function sendReply(kind, toolName, messageId, account, flags, ctx, confirmToken) {
36595
+ const metaResult = await runOrDiagnose(["gmail", "get", pos(messageId), "--format=metadata"], { account });
35562
36596
  if (metaResult.isError) return metaResult;
35563
- const headers = parseMetadataHeaders(resultText(metaResult));
36597
+ const metaText = resultText(metaResult);
36598
+ const headers = parseMetadataHeaders(metaText);
35564
36599
  const recipients = computeReplyRecipients(kind, headers, flags);
35565
- const confirmation = requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
36600
+ const confirmation = await requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
35566
36601
  messageId,
35567
36602
  recipients,
35568
36603
  recipientCount: recipients.length,
35569
36604
  subject: flags.subject || (headers.subject ? `Re: ${headers.subject}` : void 0),
35570
36605
  quoting: !flags.noQuote,
35571
36606
  bodyLength: (flags.body ?? flags.bodyHtml ?? "").length,
35572
- attachmentCount: (flags.attach?.length ?? 0) + (flags.attachInline?.length ?? 0)
36607
+ bodyPreview: bodyPreview(flags.body ?? flags.bodyHtml),
36608
+ bodyHtmlFile: flags.bodyHtmlFile,
36609
+ attachmentCount: (flags.attach?.length ?? 0) + (flags.attachInline?.length ?? 0),
36610
+ attachments: attachmentNames(flags.attach, flags.attachInline)
36611
+ }, {
36612
+ tool: toolName,
36613
+ account,
36614
+ confirmToken,
36615
+ // The metadata read above IS the phase-2 re-read: it runs on every call,
36616
+ // so the original's Message-ID/References are fresh here.
36617
+ subject: () => replyTokenSubject(kind, messageId, account, headers, metadataThreadId(metaText), flags)
35573
36618
  });
35574
36619
  if (confirmation) return confirmation;
35575
- const args = ["gmail", kind, messageId];
36620
+ const args = ["gmail", kind, pos(messageId)];
35576
36621
  appendReplyFlags(args, flags);
35577
36622
  const result = await runOrDiagnose(args, { account });
35578
36623
  if (!result.isError) logGmailDispatch(toolName, recipients, account);
35579
36624
  return result;
35580
36625
  }
36626
+ var GMAIL_RUN_BLOCKED_SETTINGS = /* @__PURE__ */ new Set(["forwarding", "autoforward", "filters", "delegates"]);
36627
+ function vetGmailRun(subcommand, args) {
36628
+ if (subcommand === "autoreply") {
36629
+ return "gog gmail autoreply sends mail and is not available through gog_gmail_run. Use gog_gmail_autoreply, which asks the user to confirm.";
36630
+ }
36631
+ if (GMAIL_RUN_BLOCKED_SETTINGS.has(subcommand)) {
36632
+ 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.`;
36633
+ }
36634
+ if (subcommand === "settings") {
36635
+ const blocked = hasCommandWord(args, GMAIL_RUN_BLOCKED_SETTINGS);
36636
+ if (blocked) {
36637
+ 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.`;
36638
+ }
36639
+ }
36640
+ return void 0;
36641
+ }
35581
36642
  function registerGmailTools(server) {
35582
36643
  server.registerTool("gog_gmail_search", {
35583
36644
  description: 'Search Gmail threads using Gmail query syntax (e.g. "from:alice subject:invoice is:unread"). The query is passed verbatim to Gmail; a bare name token (from:alison) matches per Gmail\'s own heuristics, a full address (from:alison@example.com) is exact. To match a contact across several addresses, OR them: from:(a@x.com OR b@y.com). Results are ALWAYS newest-first by Gmail\'s internalDate \u2014 the wrapper sorts them, so the first result is the most recent match and a recent message can never be buried below older ones. IMPORTANT \u2014 a response carrying "truncated": true is an INCOMPLETE view of the matches: NEVER report that a message does not exist, or that there is no such mail, on the strength of one. Page through it (pass nextPageToken back as `pageToken`), set maxPages to walk several pages in one call, or narrow the query, and only then draw a conclusion. If you already know the thread, do not search for it at all \u2014 read it directly with gog_gmail_thread_get, which returns the whole thread and cannot be truncated or mis-ranked.',
@@ -35593,7 +36654,7 @@ function registerGmailTools(server) {
35593
36654
  account: accountParam
35594
36655
  })
35595
36656
  }, async ({ query, max, pageToken, page, maxPages, all, fromContact, account }) => {
35596
- const args = ["gmail", "search", query];
36657
+ const args = ["gmail", "search", pos(query)];
35597
36658
  if (max !== void 0) args.push(`--max=${max}`);
35598
36659
  if (all) args.push("--all");
35599
36660
  if (fromContact) args.push(`--from-contact=${fromContact}`);
@@ -35624,13 +36685,13 @@ function registerGmailTools(server) {
35624
36685
  account: accountParam
35625
36686
  })
35626
36687
  }, async ({ messageId, format, sanitizeContent, account }) => {
35627
- const args = ["gmail", "get", messageId];
36688
+ const args = ["gmail", "get", pos(messageId)];
35628
36689
  if (format) args.push(`--format=${format}`);
35629
36690
  if (sanitizeContent) args.push("--sanitize-content");
35630
36691
  return runOrDiagnose(args, { account });
35631
36692
  });
35632
36693
  server.registerTool("gog_gmail_send", {
35633
- description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the recipients, subject, and body size. 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.',
36694
+ 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,
35634
36695
  annotations: { destructiveHint: true },
35635
36696
  inputSchema: external_exports.object({
35636
36697
  to: external_exports.string().describe("Recipient(s), comma-separated"),
@@ -35641,11 +36702,13 @@ function registerGmailTools(server) {
35641
36702
  replyToMessageId: external_exports.string().optional().describe('Message ID to thread this message against \u2014 sets In-Reply-To/References only. It does NOT quote the original (pass quote for that), inherit its recipients, or prefix the subject with "Re:". For an actual reply use gog_gmail_reply.'),
35642
36703
  threadId: external_exports.string().optional().describe("Thread ID to thread this message within. Same caveat as replyToMessageId: threading only, no quote and no inherited subject or recipients."),
35643
36704
  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."),
35644
- 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.`),
36705
+ 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).`),
35645
36706
  attachInline: attachInlineParam,
35646
- account: accountParam
36707
+ account: accountParam,
36708
+ confirmToken: confirmTokenParam
35647
36709
  })
35648
- }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account }, ctx) => {
36710
+ }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account, confirmToken }, ctx) => {
36711
+ confinePaths(attach, "attach");
35649
36712
  const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
35650
36713
  if (cc) args.push(`--cc=${cc}`);
35651
36714
  if (bcc) args.push(`--bcc=${bcc}`);
@@ -35656,7 +36719,7 @@ function registerGmailTools(server) {
35656
36719
  const inline = inlineAttachmentArgs("attach", attachInline, args);
35657
36720
  args.push(...inline);
35658
36721
  const recipients = extractEmails(to, cc, bcc);
35659
- const confirmation = requireGmailDispatchConfirmation(ctx, "gmail.send", {
36722
+ const confirmation = await requireGmailDispatchConfirmation(ctx, "gmail.send", {
35660
36723
  to,
35661
36724
  cc,
35662
36725
  bcc,
@@ -35664,9 +36727,37 @@ function registerGmailTools(server) {
35664
36727
  recipientCount: recipients.length,
35665
36728
  subject,
35666
36729
  bodyLength: body.length,
36730
+ bodyPreview: bodyPreview(body),
35667
36731
  threaded: Boolean(replyToMessageId || threadId),
35668
36732
  quoting: Boolean(quote),
35669
- attachmentCount: (attach?.length ?? 0) + (attachInline?.length ?? 0)
36733
+ attachmentCount: (attach?.length ?? 0) + (attachInline?.length ?? 0),
36734
+ attachments: attachmentNames(attach, attachInline)
36735
+ }, {
36736
+ tool: "gog_gmail_send",
36737
+ account,
36738
+ confirmToken,
36739
+ // Nothing is stored between the phases: the payload IS the arguments,
36740
+ // so phase 2 must repeat them and any difference is a changed send.
36741
+ subject: () => {
36742
+ const attachments = attachmentDetails(attach, attachInline);
36743
+ const from = senderPreview(account);
36744
+ return {
36745
+ target: replyToMessageId ?? threadId ?? "",
36746
+ payload: { from, to, cc, bcc, subject, body, attachments, threadId, inReplyTo: replyToMessageId, quote: Boolean(quote) },
36747
+ preview: {
36748
+ from,
36749
+ to,
36750
+ cc,
36751
+ bcc,
36752
+ subject,
36753
+ body,
36754
+ attachments: attachmentPreview(attachments),
36755
+ threadId,
36756
+ inReplyTo: replyToMessageId,
36757
+ quotesOriginal: Boolean(quote)
36758
+ }
36759
+ };
36760
+ }
35670
36761
  });
35671
36762
  if (confirmation) return confirmation;
35672
36763
  const result = await runOrDiagnose(args, { account });
@@ -35674,22 +36765,32 @@ function registerGmailTools(server) {
35674
36765
  return result;
35675
36766
  });
35676
36767
  server.registerTool("gog_gmail_reply", {
35677
- description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipient, subject, and body size. 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.',
36768
+ 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,
35678
36769
  annotations: { destructiveHint: true },
35679
36770
  inputSchema: sendReplySchema
35680
- }, async ({ messageId, account, ...flags }, ctx) => {
36771
+ }, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
35681
36772
  assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
35682
- return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx);
36773
+ confineReplyPaths(flags);
36774
+ return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx, confirmToken);
35683
36775
  });
35684
36776
  server.registerTool("gog_gmail_reply_all", {
35685
- description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipients, subject, and body size. 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.',
36777
+ 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,
35686
36778
  annotations: { destructiveHint: true },
35687
36779
  inputSchema: sendReplySchema
35688
- }, async ({ messageId, account, ...flags }, ctx) => {
36780
+ }, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
35689
36781
  assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
35690
- return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx);
36782
+ confineReplyPaths(flags);
36783
+ return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx, confirmToken);
36784
+ });
36785
+ registerRunTool(server, {
36786
+ service: "gmail",
36787
+ examples: '"archive", "mark-read", "labels"',
36788
+ // gog's --gmail-no-send blocks send/reply/forward/drafts send and all of
36789
+ // their aliases at runtime (verified on gog 0.41.0). Sending goes through the
36790
+ // confirmed tools, never this escape hatch.
36791
+ gmailNoSend: true,
36792
+ vet: vetGmailRun
35691
36793
  });
35692
- registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
35693
36794
  }
35694
36795
 
35695
36796
  // src/tools/sheets-a1.ts
@@ -35758,7 +36859,7 @@ function registerSheetsTools(server) {
35758
36859
  account: accountParam
35759
36860
  })
35760
36861
  }, async ({ spreadsheetId, range, account }) => {
35761
- return runOrDiagnose(["sheets", "get", spreadsheetId, range], { account });
36862
+ return runOrDiagnose(["sheets", "get", pos(spreadsheetId), pos(range)], { account });
35762
36863
  });
35763
36864
  server.registerTool("gog_sheets_update", {
35764
36865
  description: 'Write values to a Google Sheets range, overwriting existing content. Values may be strings, numbers, booleans, or null. Strings starting with "=" are interpreted as formulas (e.g. "=SUM(A1:A10)").',
@@ -35778,7 +36879,7 @@ function registerSheetsTools(server) {
35778
36879
  const readRange = expandAnchorRange(range, values.length, cols);
35779
36880
  let existing;
35780
36881
  try {
35781
- existing = await run(["sheets", "get", spreadsheetId, readRange], { account });
36882
+ existing = await run(["sheets", "get", pos(spreadsheetId), pos(readRange)], { account });
35782
36883
  } catch (err) {
35783
36884
  return diagnose(err);
35784
36885
  }
@@ -35790,7 +36891,7 @@ function registerSheetsTools(server) {
35790
36891
  );
35791
36892
  }
35792
36893
  }
35793
- const args = ["sheets", "update", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`];
36894
+ const args = ["sheets", "update", pos(spreadsheetId), pos(range), `--values-json=${JSON.stringify(values)}`];
35794
36895
  if (dry_run) args.push("--dry-run");
35795
36896
  if (fail_on_formula_error) args.push("--fail-on-formula-error");
35796
36897
  return runOrDiagnose(args, { account });
@@ -35806,7 +36907,7 @@ function registerSheetsTools(server) {
35806
36907
  account: accountParam
35807
36908
  })
35808
36909
  }, async ({ spreadsheetId, range, values, account, dry_run }) => {
35809
- const args = ["sheets", "append", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`];
36910
+ const args = ["sheets", "append", pos(spreadsheetId), pos(range), `--values-json=${JSON.stringify(values)}`];
35810
36911
  if (dry_run) args.push("--dry-run");
35811
36912
  return runOrDiagnose(args, { account });
35812
36913
  });
@@ -35820,7 +36921,7 @@ function registerSheetsTools(server) {
35820
36921
  account: accountParam
35821
36922
  })
35822
36923
  }, async ({ spreadsheetId, range, account, dry_run }) => {
35823
- const args = ["sheets", "clear", spreadsheetId, range];
36924
+ const args = ["sheets", "clear", pos(spreadsheetId), pos(range)];
35824
36925
  if (dry_run) args.push("--dry-run");
35825
36926
  return runOrDiagnose(args, { account });
35826
36927
  });
@@ -35832,7 +36933,7 @@ function registerSheetsTools(server) {
35832
36933
  account: accountParam
35833
36934
  })
35834
36935
  }, async ({ spreadsheetId, account }) => {
35835
- return runOrDiagnose(["sheets", "metadata", spreadsheetId], { account });
36936
+ return runOrDiagnose(["sheets", "metadata", pos(spreadsheetId)], { account });
35836
36937
  });
35837
36938
  server.registerTool("gog_sheets_create", {
35838
36939
  description: "Create a new Google Spreadsheet. Returns JSON with the new spreadsheetId and URL.",
@@ -35842,7 +36943,7 @@ function registerSheetsTools(server) {
35842
36943
  account: accountParam
35843
36944
  })
35844
36945
  }, async ({ title, account }) => {
35845
- return runOrDiagnose(["sheets", "create", title], { account });
36946
+ return runOrDiagnose(["sheets", "create", pos(title)], { account });
35846
36947
  });
35847
36948
  server.registerTool("gog_sheets_find_replace", {
35848
36949
  description: "Find and replace text across an entire Google Spreadsheet.",
@@ -35854,7 +36955,7 @@ function registerSheetsTools(server) {
35854
36955
  account: accountParam
35855
36956
  })
35856
36957
  }, async ({ spreadsheetId, find, replace, account }) => {
35857
- return runOrDiagnose(["sheets", "find-replace", spreadsheetId, find, replace], { account });
36958
+ return runOrDiagnose(["sheets", "find-replace", pos(spreadsheetId), pos(find), pos(replace)], { account });
35858
36959
  });
35859
36960
  registerRunTool(server, { service: "sheets", examples: '"freeze", "add-tab", "rename-tab"' });
35860
36961
  }
@@ -35862,8 +36963,9 @@ function registerSheetsTools(server) {
35862
36963
  // src/tools/slides.ts
35863
36964
  function registerSlidesTools(server) {
35864
36965
  server.registerTool("gog_slides_export", {
35865
- description: "Export a Google Slides presentation to a local file (pdf or pptx).",
35866
- annotations: { readOnlyHint: true },
36966
+ description: "Export a Google Slides presentation to a local file (pdf or pptx). The out path must be inside the server's GOG_FILE_ROOTS directories.",
36967
+ // Writes a file on the gog host and can overwrite one: not read-only.
36968
+ annotations: { readOnlyHint: false, destructiveHint: true },
35867
36969
  inputSchema: external_exports.object({
35868
36970
  presentationId: external_exports.string().describe("Presentation ID"),
35869
36971
  out: external_exports.string().optional().describe("Output file path"),
@@ -35872,7 +36974,8 @@ function registerSlidesTools(server) {
35872
36974
  account: accountParam
35873
36975
  })
35874
36976
  }, async ({ presentationId, out, format, overwrite, account }) => {
35875
- const args = ["slides", "export", presentationId];
36977
+ if (out) confinePath(out, "out");
36978
+ const args = ["slides", "export", pos(presentationId)];
35876
36979
  if (out) args.push(`--out=${out}`);
35877
36980
  if (format) args.push(`--format=${format}`);
35878
36981
  if (overwrite) args.push("--overwrite");
@@ -35886,7 +36989,7 @@ function registerSlidesTools(server) {
35886
36989
  account: accountParam
35887
36990
  })
35888
36991
  }, async ({ presentationId, account }) => {
35889
- return runOrDiagnose(["slides", "info", presentationId], { account });
36992
+ return runOrDiagnose(["slides", "info", pos(presentationId)], { account });
35890
36993
  });
35891
36994
  server.registerTool("gog_slides_create", {
35892
36995
  description: "Create a new Google Slides presentation, optionally in a folder or copying from a template.",
@@ -35898,7 +37001,7 @@ function registerSlidesTools(server) {
35898
37001
  account: accountParam
35899
37002
  })
35900
37003
  }, async ({ title, parent, template, account }) => {
35901
- const args = ["slides", "create", title];
37004
+ const args = ["slides", "create", pos(title)];
35902
37005
  if (parent) args.push(`--parent=${parent}`);
35903
37006
  if (template) args.push(`--template=${template}`);
35904
37007
  return runOrDiagnose(args, { account });
@@ -35913,7 +37016,7 @@ function registerSlidesTools(server) {
35913
37016
  account: accountParam
35914
37017
  })
35915
37018
  }, async ({ presentationId, title, parent, account }) => {
35916
- const args = ["slides", "copy", presentationId, title];
37019
+ const args = ["slides", "copy", pos(presentationId), pos(title)];
35917
37020
  if (parent) args.push(`--parent=${parent}`);
35918
37021
  return runOrDiagnose(args, { account });
35919
37022
  });
@@ -35925,7 +37028,7 @@ function registerSlidesTools(server) {
35925
37028
  account: accountParam
35926
37029
  })
35927
37030
  }, async ({ presentationId, account }) => {
35928
- return runOrDiagnose(["slides", "list-slides", presentationId], { account });
37031
+ return runOrDiagnose(["slides", "list-slides", pos(presentationId)], { account });
35929
37032
  });
35930
37033
  server.registerTool("gog_slides_read_slide", {
35931
37034
  description: "Read the content of a slide (text, shapes, speaker notes). Set detail=true to also include normalized element geometry, styled text runs, paragraphs, table-cell content, and image source URLs.",
@@ -35937,11 +37040,11 @@ function registerSlidesTools(server) {
35937
37040
  account: accountParam
35938
37041
  })
35939
37042
  }, async ({ presentationId, slideId, detail, account }) => {
35940
- const args = ["slides", "read-slide", presentationId, slideId];
37043
+ const args = ["slides", "read-slide", pos(presentationId), pos(slideId)];
35941
37044
  if (detail) args.push("--detail");
35942
37045
  return runOrDiagnose(args, { account });
35943
37046
  });
35944
- registerRunTool(server, { service: "slides", examples: '"add-slide", "delete-slide", "update-notes"' });
37047
+ registerRunTool(server, { service: "slides", examples: '"delete-slide", "update-notes"' });
35945
37048
  }
35946
37049
 
35947
37050
  // src/tools/tasks.ts
@@ -35963,7 +37066,7 @@ function registerTasksTools(server) {
35963
37066
  account: accountParam
35964
37067
  })
35965
37068
  }, async ({ tasklistId, account }) => {
35966
- return runOrDiagnose(["tasks", "list", tasklistId], { account });
37069
+ return runOrDiagnose(["tasks", "list", pos(tasklistId)], { account });
35967
37070
  });
35968
37071
  server.registerTool("gog_tasks_get", {
35969
37072
  description: "Get a specific task by ID.",
@@ -35974,7 +37077,7 @@ function registerTasksTools(server) {
35974
37077
  account: accountParam
35975
37078
  })
35976
37079
  }, async ({ tasklistId, taskId, account }) => {
35977
- return runOrDiagnose(["tasks", "get", tasklistId, taskId], { account });
37080
+ return runOrDiagnose(["tasks", "get", pos(tasklistId), pos(taskId)], { account });
35978
37081
  });
35979
37082
  server.registerTool("gog_tasks_add", {
35980
37083
  description: "Add a task to a Google Task list.",
@@ -35987,7 +37090,7 @@ function registerTasksTools(server) {
35987
37090
  account: accountParam
35988
37091
  })
35989
37092
  }, async ({ tasklistId, title, notes, due, account }) => {
35990
- const args = ["tasks", "add", tasklistId, `--title=${title}`];
37093
+ const args = ["tasks", "add", pos(tasklistId), `--title=${title}`];
35991
37094
  if (notes) args.push(`--notes=${notes}`);
35992
37095
  if (due) args.push(`--due=${due}`);
35993
37096
  return runOrDiagnose(args, { account });
@@ -36001,7 +37104,7 @@ function registerTasksTools(server) {
36001
37104
  account: accountParam
36002
37105
  })
36003
37106
  }, async ({ tasklistId, taskId, account }) => {
36004
- return runOrDiagnose(["tasks", "done", tasklistId, taskId], { account });
37107
+ return runOrDiagnose(["tasks", "done", pos(tasklistId), pos(taskId)], { account });
36005
37108
  });
36006
37109
  server.registerTool("gog_tasks_delete", {
36007
37110
  description: "Delete a task.",
@@ -36012,13 +37115,13 @@ function registerTasksTools(server) {
36012
37115
  account: accountParam
36013
37116
  })
36014
37117
  }, async ({ tasklistId, taskId, account }) => {
36015
- return runOrDiagnose(["tasks", "delete", tasklistId, taskId, "--force"], { account });
37118
+ return runOrDiagnose(["tasks", "delete", pos(tasklistId), pos(taskId), "--force"], { account });
36016
37119
  });
36017
37120
  registerRunTool(server, { service: "tasks", examples: '"update", "undo", "clear"' });
36018
37121
  }
36019
37122
 
36020
37123
  // src/server.ts
36021
- var VERSION = true ? "4.2.5" : "0.0.0";
37124
+ var VERSION = true ? "4.4.0" : "0.0.0";
36022
37125
  var BASE_TOOL_REGISTRARS = [
36023
37126
  registerApiTools,
36024
37127
  registerAppScriptTools,
@@ -36036,7 +37139,7 @@ var BASE_TOOL_REGISTRARS = [
36036
37139
  ];
36037
37140
 
36038
37141
  // src/bootstrap-auth.ts
36039
- import { join as join2 } from "node:path";
37142
+ import { join as join5 } from "node:path";
36040
37143
  var AUTH_BOOTSTRAP_MARKER = "auth-bootstrap.sha256";
36041
37144
  var VARS = ["GOG_CLIENT_ID", "GOG_CLIENT_SECRET", "GOG_REFRESH_TOKEN", "GOG_ACCOUNT"];
36042
37145
  var log = (line) => {
@@ -36070,12 +37173,12 @@ async function bootstrapGogAuth(env = process.env, options = {}) {
36070
37173
  const [clientId, clientSecret, refreshToken, account] = values;
36071
37174
  const { spawner } = options;
36072
37175
  try {
36073
- const { createHash } = await import("node:crypto");
37176
+ const { createHash: createHash4 } = await import("node:crypto");
36074
37177
  const { mkdir, readFile, writeFile, chmod } = await import("node:fs/promises");
36075
37178
  const home = options.home ?? (await import("node:os")).homedir();
36076
- const dir = join2(home, ".gogcli-mcp");
36077
- const marker = join2(dir, AUTH_BOOTSTRAP_MARKER);
36078
- const fingerprint = createHash("sha256").update(values.join("\0")).digest("hex");
37179
+ const dir = join5(home, ".gogcli-mcp");
37180
+ const marker = join5(dir, AUTH_BOOTSTRAP_MARKER);
37181
+ const fingerprint = createHash4("sha256").update(values.join("\0")).digest("hex");
36079
37182
  const previous = await readFile(marker, "utf8").catch(() => void 0);
36080
37183
  if (previous === fingerprint && await accountListed(account, spawner)) return "present";
36081
37184
  await run(
@@ -36100,7 +37203,7 @@ async function bootstrapGogAuth(env = process.env, options = {}) {
36100
37203
  }
36101
37204
 
36102
37205
  // src/blob-urls.ts
36103
- import { createHmac } from "node:crypto";
37206
+ import { createHmac as createHmac3 } from "node:crypto";
36104
37207
  var BLOB_URL_MAX_TTL_MS = 24 * 60 * 60 * 1e3;
36105
37208
  var BLOB_URL_CEILING_MARGIN_MS = 60 * 1e3;
36106
37209
  var BLOB_URL_DEFAULT_TTL_MS = 60 * 60 * 1e3;
@@ -36112,8 +37215,8 @@ ${exp}`;
36112
37215
  function writePayload(objectKey, contentType, exp) {
36113
37216
  return `put\0${objectKey}\0${contentType}\0${exp}`;
36114
37217
  }
36115
- function sign(signingKey, payload) {
36116
- return createHmac("sha256", signingKey).update(payload, "utf8").digest("base64url");
37218
+ function sign2(signingKey, payload) {
37219
+ return createHmac3("sha256", signingKey).update(payload, "utf8").digest("base64url");
36117
37220
  }
36118
37221
  function objectPathSegments(rest) {
36119
37222
  const segments = rest.split("/");
@@ -36149,7 +37252,7 @@ function createBlobUrlMinter(config2) {
36149
37252
  function mint(rest, exp, payload) {
36150
37253
  const segments = objectPathSegments(rest);
36151
37254
  const url2 = `${basePrefix}/${segments.map(encodeURIComponent).join("/")}`;
36152
- const signature = sign(config2.signingKey, payload(`${registrationId}/${segments.join("/")}`));
37255
+ const signature = sign2(config2.signingKey, payload(`${registrationId}/${segments.join("/")}`));
36153
37256
  return `${url2}?exp=${exp}&sig=${signature}`;
36154
37257
  }
36155
37258
  return {
@@ -36175,7 +37278,6 @@ function blobStoreFromEnv() {
36175
37278
  }
36176
37279
 
36177
37280
  // src/blob-upload.ts
36178
- var ATTACHMENT_DOWNLOAD_ROOT = "/tmp/gog-attachments";
36179
37281
  var MAX_BLOB_UPLOAD_BYTES = 100 * 1024 * 1024;
36180
37282
  var BLOB_UPLOAD_TIMEOUT_MS = 12e4;
36181
37283
  var ERROR_BODY_SNIPPET = 512;
@@ -36190,10 +37292,10 @@ var UploadError = class extends Error {
36190
37292
  async function resolveConfined(root, requested) {
36191
37293
  const path = await import("node:path");
36192
37294
  const { realpath, stat } = await import("node:fs/promises");
36193
- const isInside = (base, candidate2) => candidate2 === base || candidate2.startsWith(base + path.sep);
37295
+ const isInside3 = (base, candidate2) => candidate2 === base || candidate2.startsWith(base + path.sep);
36194
37296
  const lexicalRoot = path.resolve(root);
36195
37297
  const candidate = path.resolve(lexicalRoot, requested);
36196
- if (!isInside(lexicalRoot, candidate)) {
37298
+ if (!isInside3(lexicalRoot, candidate)) {
36197
37299
  throw new UploadError(`the attachment path must be inside ${lexicalRoot}`);
36198
37300
  }
36199
37301
  let realFile;
@@ -36204,7 +37306,7 @@ async function resolveConfined(root, requested) {
36204
37306
  } catch (err) {
36205
37307
  throw new UploadError(`the downloaded attachment could not be found: ${err.message}`);
36206
37308
  }
36207
- if (!isInside(realRoot, realFile)) {
37309
+ if (!isInside3(realRoot, realFile)) {
36208
37310
  throw new UploadError(`the attachment path must be inside ${realRoot}`);
36209
37311
  }
36210
37312
  const info = await stat(realFile);
@@ -36231,7 +37333,7 @@ async function putFile(target, file2, bytes, contentType, timeoutMs) {
36231
37333
  const { createReadStream } = await import("node:fs");
36232
37334
  const { pipeline } = await import("node:stream");
36233
37335
  const transport = target.protocol === "https:" ? await import("node:https") : await import("node:http");
36234
- return new Promise((resolve, reject) => {
37336
+ return new Promise((resolve4, reject) => {
36235
37337
  let settled = false;
36236
37338
  const source = createReadStream(file2);
36237
37339
  const req = transport.request(target, {
@@ -36261,7 +37363,7 @@ async function putFile(target, file2, bytes, contentType, timeoutMs) {
36261
37363
  held += chunk.length;
36262
37364
  });
36263
37365
  res.on("error", fail);
36264
- res.on("end", () => finish2(() => resolve({
37366
+ res.on("end", () => finish2(() => resolve4({
36265
37367
  status: res.statusCode,
36266
37368
  body: Buffer.concat(chunks).toString().slice(0, ERROR_BODY_SNIPPET)
36267
37369
  })));
@@ -36293,11 +37395,14 @@ async function uploadToBlobStore(request, options = {}) {
36293
37395
  }
36294
37396
  export {
36295
37397
  ATTACHMENT_DOWNLOAD_ROOT,
37398
+ ATTACHMENT_TTL_MS,
36296
37399
  AUTH_BOOTSTRAP_MARKER,
36297
37400
  BASE_TOOL_REGISTRARS,
36298
37401
  BLOB_UPLOAD_TIMEOUT_MS,
36299
37402
  BLOB_URL_DEFAULT_TTL_MS,
36300
37403
  BLOB_URL_MAX_TTL_MS,
37404
+ CONFIRM_FALLBACK_DESCRIPTION,
37405
+ FILE_ROOTS_ENV,
36301
37406
  INLINE_ATTACHMENT_LIMITS_TEXT,
36302
37407
  MAX_BLOB_UPLOAD_BYTES,
36303
37408
  MAX_INLINE_ATTACHMENT_BYTES,
@@ -36311,14 +37416,24 @@ export {
36311
37416
  appendReplyFlags,
36312
37417
  assertNotBoth,
36313
37418
  attachInlineParam,
37419
+ attachmentDetails,
37420
+ attachmentNames,
37421
+ attachmentPreview,
36314
37422
  authToolsFor,
36315
37423
  blobStoreFromEnv,
37424
+ bodyPreview,
36316
37425
  bootstrapGogAuth,
37426
+ confineAtFile,
37427
+ confinePath,
37428
+ confinePaths,
37429
+ confirmTokenParam,
36317
37430
  createBlobUrlMinter,
37431
+ defaultFileRoot,
36318
37432
  diagnose,
36319
37433
  errorText,
36320
37434
  extractEmails,
36321
37435
  fetchGmailPages,
37436
+ fileRoots,
36322
37437
  finalizeGmailSearch,
36323
37438
  ids,
36324
37439
  inlineAttachmentArgs,
@@ -36331,7 +37446,10 @@ export {
36331
37446
  pageTokenParam,
36332
37447
  paginationParams,
36333
37448
  payloadArg,
37449
+ pos,
37450
+ prepareDownloadRoot,
36334
37451
  pushPaginationFlags,
37452
+ readCourse,
36335
37453
  registerApiTools,
36336
37454
  registerAppScriptTools,
36337
37455
  registerAuthTools,
@@ -36346,13 +37464,16 @@ export {
36346
37464
  registerSheetsTools,
36347
37465
  registerSlidesTools,
36348
37466
  registerTasksTools,
37467
+ removeDownload,
36349
37468
  replySchema,
37469
+ requireDispatchConfirmation,
36350
37470
  requireGmailDispatchConfirmation,
36351
37471
  resolvePageToken,
36352
37472
  resultText,
36353
37473
  run,
36354
37474
  runBinary,
36355
37475
  runOrDiagnose,
37476
+ senderPreview,
36356
37477
  stripConsumedPageToken,
36357
37478
  uploadToBlobStore
36358
37479
  };