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/index.js CHANGED
@@ -2119,8 +2119,8 @@ function emoji() {
2119
2119
  }
2120
2120
  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])$/;
2121
2121
  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}|:))$/;
2122
- var mac = (delimiter2) => {
2123
- const escapedDelim = escapeRegex(delimiter2 ?? ":");
2122
+ var mac = (delimiter3) => {
2123
+ const escapedDelim = escapeRegex(delimiter3 ?? ":");
2124
2124
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
2125
2125
  };
2126
2126
  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])$/;
@@ -5148,7 +5148,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
5148
5148
  var NONE = 0;
5149
5149
  var ASSUMED = 1;
5150
5150
  var PROVEN = 2;
5151
- function isRecursive(inst, stack, resolve) {
5151
+ function isRecursive(inst, stack, resolve4) {
5152
5152
  const cached2 = recursive.get(inst);
5153
5153
  if (cached2 !== void 0)
5154
5154
  return cached2 ? PROVEN : NONE;
@@ -5158,7 +5158,7 @@ function isRecursive(inst, stack, resolve) {
5158
5158
  let result = NONE;
5159
5159
  const check2 = (child) => {
5160
5160
  if (result !== PROVEN && child?._zod) {
5161
- const answer = isRecursive(child, stack, resolve);
5161
+ const answer = isRecursive(child, stack, resolve4);
5162
5162
  if (answer > result)
5163
5163
  result = answer;
5164
5164
  }
@@ -5169,7 +5169,7 @@ function isRecursive(inst, stack, resolve) {
5169
5169
  const desc = Object.getOwnPropertyDescriptor(sh, key);
5170
5170
  if (spread && !desc.enumerable)
5171
5171
  continue;
5172
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve) : NONE;
5172
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve4) : NONE;
5173
5173
  if (child > answer)
5174
5174
  answer = child;
5175
5175
  }
@@ -5233,7 +5233,7 @@ function isRecursive(inst, stack, resolve) {
5233
5233
  break;
5234
5234
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5235
5235
  case "lazy": {
5236
- const inner = def._cachedInner ?? (resolve ? inst._zod.innerType : void 0);
5236
+ const inner = def._cachedInner ?? (resolve4 ? inst._zod.innerType : void 0);
5237
5237
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
5238
5238
  break;
5239
5239
  }
@@ -24164,14 +24164,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
24164
24164
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
24165
24165
  }
24166
24166
  function sleep(ms, signal) {
24167
- return new Promise((resolve, reject) => {
24167
+ return new Promise((resolve4, reject) => {
24168
24168
  if (signal?.aborted) {
24169
24169
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
24170
24170
  return;
24171
24171
  }
24172
24172
  const timer = setTimeout(() => {
24173
24173
  signal?.removeEventListener("abort", onAbort);
24174
- resolve();
24174
+ resolve4();
24175
24175
  }, ms);
24176
24176
  const onAbort = () => {
24177
24177
  clearTimeout(timer);
@@ -24965,7 +24965,7 @@ var Protocol = class {
24965
24965
  const flowStartedAt = Date.now();
24966
24966
  let onAbort;
24967
24967
  let cleanupMessageId;
24968
- return new Promise((resolve, reject) => {
24968
+ return new Promise((resolve4, reject) => {
24969
24969
  const earlyReject = (error62) => {
24970
24970
  reject(error62);
24971
24971
  };
@@ -25033,7 +25033,7 @@ var Protocol = class {
25033
25033
  }
25034
25034
  if (decoded.kind === "invalid") return reject(decoded.error);
25035
25035
  if (decoded.kind === "input_required") {
25036
- if (options?.allowInputRequired === true) return resolve(manualInputRequiredValue(decoded));
25036
+ if (options?.allowInputRequired === true) return resolve4(manualInputRequiredValue(decoded));
25037
25037
  const flow = {
25038
25038
  codec: codec2,
25039
25039
  request,
@@ -25045,11 +25045,11 @@ var Protocol = class {
25045
25045
  params
25046
25046
  }, resultSchema, legOptions)
25047
25047
  };
25048
- return resolve(this._resolveNonCompleteResult(decoded, flow));
25048
+ return resolve4(this._resolveNonCompleteResult(decoded, flow));
25049
25049
  }
25050
25050
  const result = decoded.result;
25051
25051
  validateStandardSchema(resultSchema, result).then((parseResult) => {
25052
- if (parseResult.success) resolve(parseResult.data);
25052
+ if (parseResult.success) resolve4(parseResult.data);
25053
25053
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
25054
25054
  }, reject);
25055
25055
  });
@@ -27961,7 +27961,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
27961
27961
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
27962
27962
  const schOrFunc = root.refs[ref];
27963
27963
  if (schOrFunc) return schOrFunc;
27964
- let _sch = resolve.call(this, root, ref);
27964
+ let _sch = resolve4.call(this, root, ref);
27965
27965
  if (_sch === void 0) {
27966
27966
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
27967
27967
  const { schemaId } = this.opts;
@@ -27987,7 +27987,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
27987
27987
  function sameSchemaEnv(s1, s2) {
27988
27988
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
27989
27989
  }
27990
- function resolve(root, ref) {
27990
+ function resolve4(root, ref) {
27991
27991
  let sch;
27992
27992
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
27993
27993
  return sch || this.schemas[ref] || resolveSchema.call(this, root, ref);
@@ -28437,47 +28437,47 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
28437
28437
  else if (typeof uri === "object") uri = parse3(serialize(uri, options), options);
28438
28438
  return uri;
28439
28439
  }
28440
- function resolve(baseURI, relativeURI, options) {
28440
+ function resolve4(baseURI, relativeURI, options) {
28441
28441
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
28442
28442
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
28443
28443
  schemelessOptions.skipEscape = true;
28444
28444
  return serialize(resolved, schemelessOptions);
28445
28445
  }
28446
- function resolveComponent(base, relative, options, skipNormalization) {
28446
+ function resolveComponent(base, relative3, options, skipNormalization) {
28447
28447
  const target = {};
28448
28448
  if (!skipNormalization) {
28449
28449
  base = parse3(serialize(base, options), options);
28450
- relative = parse3(serialize(relative, options), options);
28450
+ relative3 = parse3(serialize(relative3, options), options);
28451
28451
  }
28452
28452
  options = options || {};
28453
- if (!options.tolerant && relative.scheme) {
28454
- target.scheme = relative.scheme;
28455
- target.userinfo = relative.userinfo;
28456
- target.host = relative.host;
28457
- target.port = relative.port;
28458
- target.path = removeDotSegments(relative.path || "");
28459
- target.query = relative.query;
28453
+ if (!options.tolerant && relative3.scheme) {
28454
+ target.scheme = relative3.scheme;
28455
+ target.userinfo = relative3.userinfo;
28456
+ target.host = relative3.host;
28457
+ target.port = relative3.port;
28458
+ target.path = removeDotSegments(relative3.path || "");
28459
+ target.query = relative3.query;
28460
28460
  } else {
28461
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
28462
- target.userinfo = relative.userinfo;
28463
- target.host = relative.host;
28464
- target.port = relative.port;
28465
- target.path = removeDotSegments(relative.path || "");
28466
- target.query = relative.query;
28461
+ if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
28462
+ target.userinfo = relative3.userinfo;
28463
+ target.host = relative3.host;
28464
+ target.port = relative3.port;
28465
+ target.path = removeDotSegments(relative3.path || "");
28466
+ target.query = relative3.query;
28467
28467
  } else {
28468
- if (!relative.path) {
28468
+ if (!relative3.path) {
28469
28469
  target.path = base.path;
28470
- if (relative.query !== void 0) target.query = relative.query;
28470
+ if (relative3.query !== void 0) target.query = relative3.query;
28471
28471
  else target.query = base.query;
28472
28472
  } else {
28473
- if (relative.path[0] === "/") target.path = removeDotSegments(relative.path);
28473
+ if (relative3.path[0] === "/") target.path = removeDotSegments(relative3.path);
28474
28474
  else {
28475
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative.path;
28476
- else if (!base.path) target.path = relative.path;
28477
- else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
28475
+ if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative3.path;
28476
+ else if (!base.path) target.path = relative3.path;
28477
+ else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
28478
28478
  target.path = removeDotSegments(target.path);
28479
28479
  }
28480
- target.query = relative.query;
28480
+ target.query = relative3.query;
28481
28481
  }
28482
28482
  target.userinfo = base.userinfo;
28483
28483
  target.host = base.host;
@@ -28485,7 +28485,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
28485
28485
  }
28486
28486
  target.scheme = base.scheme;
28487
28487
  }
28488
- target.fragment = relative.fragment;
28488
+ target.fragment = relative3.fragment;
28489
28489
  return target;
28490
28490
  }
28491
28491
  function equal(uriA, uriB, options) {
@@ -28611,7 +28611,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
28611
28611
  const fastUri = {
28612
28612
  SCHEMES,
28613
28613
  normalize,
28614
- resolve,
28614
+ resolve: resolve4,
28615
28615
  resolveComponent,
28616
28616
  equal,
28617
28617
  serialize,
@@ -29393,14 +29393,14 @@ var require_ucs2length = /* @__PURE__ */ __commonJSMin(((exports) => {
29393
29393
  function ucs2length(str) {
29394
29394
  const len = str.length;
29395
29395
  let length = 0;
29396
- let pos = 0;
29396
+ let pos2 = 0;
29397
29397
  let value;
29398
- while (pos < len) {
29398
+ while (pos2 < len) {
29399
29399
  length++;
29400
- value = str.charCodeAt(pos++);
29401
- if (value >= 55296 && value <= 56319 && pos < len) {
29402
- value = str.charCodeAt(pos);
29403
- if ((value & 64512) === 56320) pos++;
29400
+ value = str.charCodeAt(pos2++);
29401
+ if (value >= 55296 && value <= 56319 && pos2 < len) {
29402
+ value = str.charCodeAt(pos2);
29403
+ if ((value & 64512) === 56320) pos2++;
29404
29404
  }
29405
29405
  }
29406
29406
  return length;
@@ -33984,7 +33984,7 @@ var StdioServerTransport = class {
33984
33984
  }
33985
33985
  send(message) {
33986
33986
  if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
33987
- return new Promise((resolve, reject) => {
33987
+ return new Promise((resolve4, reject) => {
33988
33988
  const json2 = serializeMessage(message);
33989
33989
  let settled = false;
33990
33990
  const onError = (error62) => {
@@ -33999,14 +33999,14 @@ var StdioServerTransport = class {
33999
33999
  settled = true;
34000
34000
  this._stdout.off("error", onError);
34001
34001
  this._stdout.off("drain", onDrain);
34002
- resolve();
34002
+ resolve4();
34003
34003
  };
34004
34004
  this._stdout.once("error", onError);
34005
34005
  if (this._stdout.write(json2)) {
34006
34006
  if (settled) return;
34007
34007
  settled = true;
34008
34008
  this._stdout.off("error", onError);
34009
- resolve();
34009
+ resolve4();
34010
34010
  } else if (!settled) this._stdout.once("drain", onDrain);
34011
34011
  });
34012
34012
  }
@@ -34060,14 +34060,14 @@ var StdioConnectionChannel = class {
34060
34060
  */
34061
34061
  async whenRequestsAnswered(timeoutMs) {
34062
34062
  if (this._closed || this._pendingRequests.size === 0) return true;
34063
- return await new Promise((resolve) => {
34063
+ return await new Promise((resolve4) => {
34064
34064
  const waiter = () => {
34065
34065
  clearTimeout(timer);
34066
- resolve(true);
34066
+ resolve4(true);
34067
34067
  };
34068
34068
  const timer = setTimeout(() => {
34069
34069
  this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
34070
- resolve(false);
34070
+ resolve4(false);
34071
34071
  }, timeoutMs);
34072
34072
  this._drainWaiters.push(waiter);
34073
34073
  });
@@ -34206,14 +34206,14 @@ function serveStdio(factory, options = {}) {
34206
34206
  return false;
34207
34207
  };
34208
34208
  const answerLegacyRejection = (request, reason, requestedVersion) => {
34209
- const rejection2 = modernOnlyStrictRejection({
34209
+ const rejection3 = modernOnlyStrictRejection({
34210
34210
  kind: "legacy",
34211
34211
  reason,
34212
34212
  ...requestedVersion !== void 0 && { requestedVersion }
34213
34213
  }, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
34214
- if (rejection2 === void 0) return Promise.resolve();
34215
- reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection2.cell}): ${rejection2.message}`));
34216
- return writeErrorResponse(request.id, rejection2.code, rejection2.message, rejection2.data);
34214
+ if (rejection3 === void 0) return Promise.resolve();
34215
+ reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection3.cell}): ${rejection3.message}`));
34216
+ return writeErrorResponse(request.id, rejection3.code, rejection3.message, rejection3.data);
34217
34217
  };
34218
34218
  const onInstanceClosed = (channel) => {
34219
34219
  if (closing || channel === discarding) return;
@@ -34437,13 +34437,28 @@ var API_KEY_RE = new RegExp([
34437
34437
  "whsec_[A-Za-z0-9]{16,}\\b"
34438
34438
  // webhook signing secret (Stripe-style)
34439
34439
  ].map((p) => `\\b${p}`).join("|"), "g");
34440
- var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
34440
+ 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;
34441
+ var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
34442
+ var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
34443
+ var HEADER_SECRET_RE = /(\b(?:x[-_][a-z0-9_-]*?(?:api[-_]?key|token|secret|auth)[a-z0-9_-]*|api[-_]?key)\s*:\s*)[^\s,;"'<>`]+/gi;
34441
34444
  var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
34442
- var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
34443
- var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
34444
- var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
34445
+ var JSON_SECRET_KEYS = [
34446
+ "(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
34447
+ "client[_-]?secret",
34448
+ "api[_-]?secret",
34449
+ "(?:x[_-])?api[_-]?key",
34450
+ "private[_-]?key",
34451
+ "x[_-][a-z0-9_-]*?(?:api[_-]?key|token|secret|auth)[a-z0-9_-]*",
34452
+ "(?:proxy-)?authorization",
34453
+ "password",
34454
+ "passwd",
34455
+ "secret",
34456
+ "token"
34457
+ ].join("|");
34458
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
34459
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')(?:[^'\\\\]|\\\\.)*(')`, "gi");
34445
34460
  function redactSecrets(text) {
34446
- 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]");
34461
+ 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]");
34447
34462
  }
34448
34463
 
34449
34464
  // ../../node_modules/@chrischall/mcp-utils/dist/response/view.js
@@ -34538,26 +34553,105 @@ function errorResult(message) {
34538
34553
  }
34539
34554
 
34540
34555
  // ../../node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
34556
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
34541
34557
  var DEFAULT_REQUEST_KEY = "confirmation";
34542
34558
  var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
34543
34559
  var UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
34560
+ var STATE_PREFIX = "mcpu.confirm.v1.";
34561
+ function canonicalJson(value) {
34562
+ if (value === null || typeof value === "string" || typeof value === "boolean")
34563
+ return JSON.stringify(value);
34564
+ if (value === void 0)
34565
+ return "null";
34566
+ if (typeof value === "number")
34567
+ return Number.isFinite(value) ? JSON.stringify(value) : `{"$num":"${String(value)}"}`;
34568
+ if (typeof value === "bigint")
34569
+ return `{"$bigint":"${value.toString()}"}`;
34570
+ if (typeof value === "function" || typeof value === "symbol") {
34571
+ throw new TypeError(`requireConfirmation: cannot bind a ${typeof value} argument value.`);
34572
+ }
34573
+ if (Array.isArray(value))
34574
+ return `[${value.map(canonicalJson).join(",")}]`;
34575
+ if (value instanceof Date) {
34576
+ return `{"$date":${JSON.stringify(Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString())}}`;
34577
+ }
34578
+ if (ArrayBuffer.isView(value)) {
34579
+ const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
34580
+ return `{"$bytes":${JSON.stringify(bytes.toString("base64"))}}`;
34581
+ }
34582
+ if (value instanceof ArrayBuffer)
34583
+ return `{"$bytes":${JSON.stringify(Buffer.from(value).toString("base64"))}}`;
34584
+ if (value instanceof Map) {
34585
+ const entries2 = [...value.entries()].map(([k, v]) => `[${canonicalJson(k)},${canonicalJson(v)}]`).sort();
34586
+ return `{"$map":[${entries2.join(",")}]}`;
34587
+ }
34588
+ if (value instanceof Set) {
34589
+ return `{"$set":[${[...value].map(canonicalJson).sort().join(",")}]}`;
34590
+ }
34591
+ const proto = Object.getPrototypeOf(value);
34592
+ if (proto !== Object.prototype && proto !== null) {
34593
+ throw new TypeError("requireConfirmation: cannot bind a non-plain object argument value (class instance).");
34594
+ }
34595
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
34596
+ return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
34597
+ }
34598
+ function escapeKey(key) {
34599
+ return key.startsWith("$") ? `$${key}` : key;
34600
+ }
34601
+ function bindingKey(binding) {
34602
+ if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
34603
+ throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
34604
+ }
34605
+ const key = typeof binding.key === "string" ? Buffer.from(binding.key, "utf8") : Buffer.from(binding.key);
34606
+ if (key.length < 32) {
34607
+ throw new RangeError("requireConfirmation: binding.key must be at least 32 bytes.");
34608
+ }
34609
+ return key;
34610
+ }
34611
+ function commitment(action, args) {
34612
+ return createHash("sha256").update(`${action}\0${canonicalJson(args)}`).digest("base64url");
34613
+ }
34614
+ function mintState(key, action, binding) {
34615
+ const exp = Math.floor(Date.now() / 1e3) + (binding.ttlSeconds ?? 600);
34616
+ const body = Buffer.from(JSON.stringify({ c: commitment(action, binding.args), exp })).toString("base64url");
34617
+ const mac3 = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest("base64url");
34618
+ return `${STATE_PREFIX}${body}.${mac3}`;
34619
+ }
34620
+ function verifyState(key, action, binding, state) {
34621
+ if (typeof state !== "string" || !state.startsWith(STATE_PREFIX))
34622
+ return false;
34623
+ const rest = state.slice(STATE_PREFIX.length);
34624
+ const dot = rest.indexOf(".");
34625
+ if (dot < 0)
34626
+ return false;
34627
+ const body = rest.slice(0, dot);
34628
+ const mac3 = Buffer.from(rest.slice(dot + 1), "base64url");
34629
+ const expected = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest();
34630
+ if (mac3.length !== expected.length || !timingSafeEqual(mac3, expected))
34631
+ return false;
34632
+ let payload;
34633
+ try {
34634
+ payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
34635
+ } catch {
34636
+ return false;
34637
+ }
34638
+ if (typeof payload.exp !== "number" || payload.exp * 1e3 <= Date.now())
34639
+ return false;
34640
+ const want = Buffer.from(commitment(action, binding.args));
34641
+ const got = Buffer.from(typeof payload.c === "string" ? payload.c : "");
34642
+ return got.length === want.length && timingSafeEqual(got, want);
34643
+ }
34644
+ function echoedState(ctx) {
34645
+ const accessor = ctx.mcpReq.requestState;
34646
+ return typeof accessor === "function" ? accessor() : void 0;
34647
+ }
34544
34648
  function requireConfirmation(ctx, options) {
34545
34649
  const requestKey = options.requestKey ?? DEFAULT_REQUEST_KEY;
34650
+ const key = options.binding ? bindingKey(options.binding) : void 0;
34546
34651
  const confirmationSchema = external_exports.object({
34547
34652
  confirmed: external_exports.boolean().describe(options.confirmationLabel ?? DEFAULT_CONFIRMATION_LABEL)
34548
34653
  });
34549
- const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
34550
- const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
34551
- if (response.kind === "missing") {
34552
- if (callerAcceptsFormElicitation(ctx) === false) {
34553
- return textResult({
34554
- confirmed: false,
34555
- dispatched: false,
34556
- action: options.action,
34557
- reason: "confirmation-unsupported",
34558
- note: options.unsupportedNote ? `${UNSUPPORTED_NOTE}. ${options.unsupportedNote}` : `${UNSUPPORTED_NOTE}.`
34559
- });
34560
- }
34654
+ const ask = () => {
34561
34655
  const preview = {
34562
34656
  action: options.action,
34563
34657
  ...options.details === void 0 ? {} : { details: options.details }
@@ -34569,10 +34663,33 @@ function requireConfirmation(ctx, options) {
34569
34663
  ${JSON.stringify(preview, null, 2)}`,
34570
34664
  requestedSchema: confirmationSchema
34571
34665
  })
34572
- }
34666
+ },
34667
+ ...key && options.binding ? { requestState: mintState(key, options.action, options.binding) } : {}
34573
34668
  });
34669
+ };
34670
+ const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
34671
+ const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
34672
+ if (response.kind === "missing") {
34673
+ if (callerAcceptsFormElicitation(ctx) === false) {
34674
+ return textResult({
34675
+ confirmed: false,
34676
+ dispatched: false,
34677
+ action: options.action,
34678
+ reason: "confirmation-unsupported",
34679
+ note: options.unsupportedNote ? `${UNSUPPORTED_NOTE}. ${options.unsupportedNote}` : `${UNSUPPORTED_NOTE}.`
34680
+ });
34681
+ }
34682
+ return ask();
34574
34683
  }
34575
34684
  if (response.kind === "elicit" && response.action === "accept" && accepted?.confirmed === true) {
34685
+ if (key && options.binding) {
34686
+ const state = echoedState(ctx);
34687
+ if (state === void 0 || state === null) {
34688
+ 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.`);
34689
+ }
34690
+ if (!verifyState(key, options.action, options.binding, state))
34691
+ return ask();
34692
+ }
34576
34693
  return void 0;
34577
34694
  }
34578
34695
  return textResult({
@@ -34677,13 +34794,47 @@ async function createMcpServer(opts) {
34677
34794
  }
34678
34795
  return server;
34679
34796
  }
34797
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
34798
+ var DEFAULT_REPEAT_SIGNAL_GRACE_MS = 500;
34680
34799
  function withGracefulShutdown(target, opts = {}) {
34681
34800
  const shouldExit = opts.exit ?? true;
34801
+ if (opts.timeoutMs !== void 0 && !(Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0)) {
34802
+ throw new RangeError("withGracefulShutdown: timeoutMs must be a finite number greater than 0.");
34803
+ }
34804
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
34805
+ if (opts.repeatSignalGraceMs !== void 0 && !(Number.isFinite(opts.repeatSignalGraceMs) && opts.repeatSignalGraceMs >= 0)) {
34806
+ throw new RangeError("withGracefulShutdown: repeatSignalGraceMs must be a finite number >= 0.");
34807
+ }
34808
+ const repeatGraceMs = opts.repeatSignalGraceMs ?? DEFAULT_REPEAT_SIGNAL_GRACE_MS;
34682
34809
  let shuttingDown = false;
34810
+ let firstSignalAt = 0;
34811
+ let exited = false;
34812
+ const exitOnce = () => {
34813
+ if (exited)
34814
+ return;
34815
+ exited = true;
34816
+ process.exit(0);
34817
+ };
34683
34818
  const handler = (signal) => {
34684
- if (shuttingDown)
34819
+ if (shuttingDown) {
34820
+ if (Date.now() - firstSignalAt < repeatGraceMs)
34821
+ return;
34822
+ if (shouldExit) {
34823
+ console.error(`[mcp-utils] second ${signal} during shutdown \u2014 exiting now`);
34824
+ exitOnce();
34825
+ }
34685
34826
  return;
34827
+ }
34686
34828
  shuttingDown = true;
34829
+ firstSignalAt = Date.now();
34830
+ let timer;
34831
+ if (shouldExit) {
34832
+ timer = setTimeout(() => {
34833
+ console.error(`[mcp-utils] graceful shutdown on ${signal} did not finish in ${timeoutMs}ms \u2014 exiting`);
34834
+ exitOnce();
34835
+ }, timeoutMs);
34836
+ timer.unref?.();
34837
+ }
34687
34838
  void (async () => {
34688
34839
  try {
34689
34840
  if (opts.onSignal)
@@ -34692,8 +34843,10 @@ function withGracefulShutdown(target, opts = {}) {
34692
34843
  } catch (err) {
34693
34844
  console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
34694
34845
  } finally {
34846
+ if (timer !== void 0)
34847
+ clearTimeout(timer);
34695
34848
  if (shouldExit)
34696
- process.exit(0);
34849
+ exitOnce();
34697
34850
  }
34698
34851
  })();
34699
34852
  };
@@ -34723,6 +34876,8 @@ function runMcp(opts) {
34723
34876
  }
34724
34877
 
34725
34878
  // ../../node_modules/@chrischall/mcp-utils/dist/config/index.js
34879
+ import { homedir } from "node:os";
34880
+ import { isAbsolute, join, resolve } from "node:path";
34726
34881
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
34727
34882
  function readEnvVar(key, opts = {}) {
34728
34883
  const env = opts.env ?? process.env;
@@ -34749,6 +34904,50 @@ function parseBoolEnv(key, opts = {}) {
34749
34904
  return false;
34750
34905
  return fallback;
34751
34906
  }
34907
+ function expandPath(p) {
34908
+ let expanded = p;
34909
+ if (p === "~") {
34910
+ expanded = homedir();
34911
+ } else if (p.startsWith("~/")) {
34912
+ expanded = join(homedir(), p.slice(2));
34913
+ }
34914
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
34915
+ }
34916
+
34917
+ // ../../node_modules/@chrischall/mcp-utils/dist/fs/confine.js
34918
+ import { existsSync, realpathSync } from "node:fs";
34919
+ import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
34920
+ function isInside(root, target) {
34921
+ const rel = relative(root, target);
34922
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute2(rel);
34923
+ }
34924
+ function realPathAllowingMissing(p) {
34925
+ const abs = resolve2(p);
34926
+ let existing = abs;
34927
+ const tail = [];
34928
+ while (!existsSync(existing)) {
34929
+ const parent = dirname(existing);
34930
+ if (parent === existing)
34931
+ break;
34932
+ tail.unshift(existing.slice(parent.length).replace(/^[/\\]+/, ""));
34933
+ existing = parent;
34934
+ }
34935
+ let real;
34936
+ try {
34937
+ real = realpathSync(existing);
34938
+ } catch {
34939
+ real = existing;
34940
+ }
34941
+ return tail.length > 0 ? resolve2(real, ...tail) : real;
34942
+ }
34943
+ function assertPathWithinRoots(path, roots) {
34944
+ const target = realPathAllowingMissing(expandPath(path));
34945
+ for (const root of roots) {
34946
+ if (isInside(realPathAllowingMissing(expandPath(root)), target))
34947
+ return target;
34948
+ }
34949
+ throw new Error(`Path is outside the allowed directories: ${path}`);
34950
+ }
34752
34951
 
34753
34952
  // ../../node_modules/@chrischall/mcp-utils/dist/zod/index.js
34754
34953
  var PositiveInt = external_exports.number().int().positive();
@@ -34770,7 +34969,7 @@ var pageSchema = {
34770
34969
  };
34771
34970
 
34772
34971
  // src/runner.ts
34773
- import { delimiter, join } from "node:path";
34972
+ import { delimiter, join as join2 } from "node:path";
34774
34973
 
34775
34974
  // src/timestamps.ts
34776
34975
  var DEFAULT_DISPLAY_TZ = "America/New_York";
@@ -34796,9 +34995,9 @@ function offsetAt(instant, tz) {
34796
34995
  const w = wallPartsIn(instant, tz);
34797
34996
  const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
34798
34997
  const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
34799
- const sign = minutes < 0 ? "-" : "+";
34998
+ const sign2 = minutes < 0 ? "-" : "+";
34800
34999
  const abs = Math.abs(minutes);
34801
- return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
35000
+ return `${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
34802
35001
  }
34803
35002
  function wallPartsIn(instant, tz) {
34804
35003
  const parts = new Intl.DateTimeFormat("en-US", {
@@ -34971,9 +35170,48 @@ function normalizeTimestamps(text, tz = displayTimeZone(), naiveTz = naiveSource
34971
35170
  return JSON.stringify(parsed, null, detectIndent(text));
34972
35171
  }
34973
35172
 
35173
+ // src/arg-guard.ts
35174
+ 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;
35175
+ var SHORT_ACCOUNT_CLUSTER = /^-(?!-)[A-Za-z]*a/;
35176
+ function forbiddenArgReason(arg) {
35177
+ if (arg === "--") {
35178
+ return 'The argument "--" is not allowed: it ends flag parsing and would disable safety flags appended after it.';
35179
+ }
35180
+ const match = FORBIDDEN_LONG_FLAG.exec(arg);
35181
+ if (match) {
35182
+ 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.`;
35183
+ }
35184
+ if (SHORT_ACCOUNT_CLUSTER.test(arg)) {
35185
+ return `The flag ${arg} is not allowed here: -a selects the account. Use the tool's account parameter instead.`;
35186
+ }
35187
+ return void 0;
35188
+ }
35189
+ function assertSafeForwardedArgs(args) {
35190
+ for (const arg of args) {
35191
+ const reason = forbiddenArgReason(arg);
35192
+ if (reason) throw new Error(reason);
35193
+ }
35194
+ }
35195
+ var SUBCOMMAND_SHAPE = /^[a-z][a-z0-9-]*$/;
35196
+ function assertSafeSubcommand(subcommand) {
35197
+ if (!SUBCOMMAND_SHAPE.test(subcommand)) {
35198
+ throw new Error(
35199
+ `Invalid subcommand ${JSON.stringify(subcommand)}: pass a single gog subcommand name such as "list" or "mark-read", and put its arguments in args.`
35200
+ );
35201
+ }
35202
+ }
35203
+
35204
+ // src/argv.ts
35205
+ function pos(value) {
35206
+ return { kind: "positional", value };
35207
+ }
35208
+ function isGogPositional(arg) {
35209
+ return typeof arg === "object" && arg !== null && arg.kind === "positional";
35210
+ }
35211
+
34974
35212
  // src/runner.ts
34975
35213
  function isGogFileArg(arg) {
34976
- return typeof arg !== "string";
35214
+ return typeof arg !== "string" && arg.kind === "file";
34977
35215
  }
34978
35216
  var TIMEOUT_MS = 3e4;
34979
35217
  function readonlyEnvEnabled() {
@@ -35057,8 +35295,8 @@ function formatTimeout(ms) {
35057
35295
  }
35058
35296
  async function spawnWithTempFiles(args, opts) {
35059
35297
  const { mkdtemp, mkdir, writeFile, rm } = await import("node:fs/promises");
35060
- const { tmpdir } = await import("node:os");
35061
- const dir = await mkdtemp(join(tmpdir(), "gogcli-mcp-"));
35298
+ const { tmpdir: tmpdir2 } = await import("node:os");
35299
+ const dir = await mkdtemp(join2(tmpdir2(), "gogcli-mcp-"));
35062
35300
  try {
35063
35301
  const argv = [];
35064
35302
  let seq = 0;
@@ -35067,10 +35305,10 @@ async function spawnWithTempFiles(args, opts) {
35067
35305
  argv.push(arg);
35068
35306
  continue;
35069
35307
  }
35070
- const sub = join(dir, String(seq));
35308
+ const sub = join2(dir, String(seq));
35071
35309
  seq += 1;
35072
35310
  await mkdir(sub, { recursive: true, mode: 448 });
35073
- const path = join(sub, arg.filename ?? `${arg.flag}.${arg.ext ?? "txt"}`);
35311
+ const path = join2(sub, arg.filename ?? `${arg.flag}.${arg.ext ?? "txt"}`);
35074
35312
  const data = arg.encoding === "base64" ? Buffer.from(arg.contents, "base64") : Buffer.from(arg.contents, "utf8");
35075
35313
  await writeFile(path, data, { mode: 384 });
35076
35314
  argv.push(arg.positional ? path : `--${arg.flag}=${path}`);
@@ -35088,10 +35326,10 @@ function spawnExecutor(args, opts) {
35088
35326
  return spawnGog(args, opts);
35089
35327
  }
35090
35328
  async function spawnGog(fullArgs, opts) {
35091
- const { timeout, interactive = false, spawner, binary = false } = opts;
35329
+ const { timeout, interactive = false, spawner, binary = false, maxOutputBytes } = opts;
35092
35330
  const spawn = spawner ?? (await import("node:child_process")).spawn;
35093
35331
  const effectiveTimeout = timeout ?? TIMEOUT_MS;
35094
- return new Promise((resolve, reject) => {
35332
+ return new Promise((resolve4, reject) => {
35095
35333
  const childEnv = { ...sanitizedEnv(), GOG_TIMEZONE: naiveSourceTimeZone(), PATH: augmentedPath() };
35096
35334
  const child = spawn(readEnvVar("GOG_PATH") ?? "gog", fullArgs, { env: childEnv });
35097
35335
  const stdoutChunks = [];
@@ -35109,7 +35347,17 @@ async function spawnGog(fullArgs, opts) {
35109
35347
  child.kill();
35110
35348
  reject(new Error(`gog timed out after ${formatTimeout(effectiveTimeout)}`));
35111
35349
  }, effectiveTimeout);
35350
+ let stdoutBytes = 0;
35112
35351
  child.stdout.on("data", (chunk) => {
35352
+ if (settled) return;
35353
+ stdoutBytes += chunk.length;
35354
+ if (maxOutputBytes !== void 0 && stdoutBytes > maxOutputBytes) {
35355
+ settled = true;
35356
+ stopWatching();
35357
+ child.kill();
35358
+ reject(new Error(`gog output exceeded the limit: more than ${maxOutputBytes} bytes`));
35359
+ return;
35360
+ }
35113
35361
  stdoutChunks.push(chunk);
35114
35362
  });
35115
35363
  child.stderr.on("data", (chunk) => {
@@ -35126,14 +35374,14 @@ async function spawnGog(fullArgs, opts) {
35126
35374
  const stderr = Buffer.concat(stderrChunks).toString().trim();
35127
35375
  if (code === 0) {
35128
35376
  if (binary) {
35129
- resolve(Buffer.concat(stdoutChunks).toString("base64"));
35377
+ resolve4(Buffer.concat(stdoutChunks).toString("base64"));
35130
35378
  return;
35131
35379
  }
35132
35380
  const stdout = Buffer.concat(stdoutChunks).toString();
35133
35381
  if (interactive && stderr) {
35134
- resolve(stdout + "\n" + stderr);
35382
+ resolve4(stdout + "\n" + stderr);
35135
35383
  } else {
35136
- resolve(stdout);
35384
+ resolve4(stdout);
35137
35385
  }
35138
35386
  } else {
35139
35387
  reject(new Error(stderr || `gog exited with code ${code}`));
@@ -35153,7 +35401,33 @@ async function spawnGog(fullArgs, opts) {
35153
35401
  });
35154
35402
  });
35155
35403
  }
35404
+ function placePositionals(args) {
35405
+ const flags = [];
35406
+ const positionals = [];
35407
+ let afterSeparator = false;
35408
+ for (const arg of args) {
35409
+ if (arg === "--") {
35410
+ afterSeparator = true;
35411
+ } else if (isGogPositional(arg)) {
35412
+ positionals.push(arg.value);
35413
+ } else if (afterSeparator || isGogFileArg(arg) && arg.positional) {
35414
+ positionals.push(arg);
35415
+ } else {
35416
+ flags.push(arg);
35417
+ }
35418
+ }
35419
+ return { flags, positionals };
35420
+ }
35421
+ function assertNoSafetyOverrides(flags) {
35422
+ for (const arg of flags) {
35423
+ if (typeof arg !== "string") continue;
35424
+ const reason = forbiddenArgReason(arg);
35425
+ if (reason) throw new Error(reason);
35426
+ }
35427
+ }
35156
35428
  function assembleArgs(args, opts) {
35429
+ const { flags, positionals } = placePositionals(args);
35430
+ assertNoSafetyOverrides(flags);
35157
35431
  const effectiveAccount = opts.account ?? readEnvVar("GOG_ACCOUNT");
35158
35432
  const fullArgs = ["--json", "--color=never"];
35159
35433
  if (!opts.interactive) {
@@ -35162,17 +35436,21 @@ function assembleArgs(args, opts) {
35162
35436
  if (opts.readonly || readonlyEnvEnabled()) {
35163
35437
  fullArgs.push("--readonly");
35164
35438
  }
35439
+ if (opts.gmailNoSend) {
35440
+ fullArgs.push("--gmail-no-send");
35441
+ }
35165
35442
  if (effectiveAccount) {
35166
35443
  fullArgs.push("--account", effectiveAccount);
35167
35444
  }
35168
- fullArgs.push(...args);
35445
+ fullArgs.push(...flags);
35446
+ if (positionals.length > 0) fullArgs.push("--", ...positionals);
35169
35447
  return fullArgs;
35170
35448
  }
35171
35449
  async function run(args, options = {}) {
35172
- const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full", opaqueFields } = options;
35450
+ const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full", opaqueFields, gmailNoSend = false } = options;
35173
35451
  const base = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
35174
35452
  const redact = opaqueFields?.length ? (text) => redactPreservingOpaqueFields(text, opaqueFields, base) : base;
35175
- const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
35453
+ const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2, gmailNoSend });
35176
35454
  try {
35177
35455
  return redact(await spawnExecutor(fullArgs, { timeout, interactive, spawner }));
35178
35456
  } catch (err) {
@@ -35180,9 +35458,9 @@ async function run(args, options = {}) {
35180
35458
  }
35181
35459
  }
35182
35460
  async function runBinary(args, options = {}) {
35183
- const { account, spawner, timeout, readonly: readonly2 = false } = options;
35184
- const fullArgs = assembleArgs(args, { account, interactive: false, readonly: readonly2 });
35185
- return spawnExecutor(fullArgs, { timeout, interactive: false, spawner, binary: true });
35461
+ const { account, spawner, timeout, readonly: readonly2 = false, gmailNoSend = false, maxOutputBytes } = options;
35462
+ const fullArgs = assembleArgs(args, { account, interactive: false, readonly: readonly2, gmailNoSend });
35463
+ return spawnExecutor(fullArgs, { timeout, interactive: false, spawner, binary: true, maxOutputBytes });
35186
35464
  }
35187
35465
 
35188
35466
  // src/pagination.ts
@@ -35234,6 +35512,114 @@ function annotateTruncatedList(result, itemsKey) {
35234
35512
  return rawTextResult(JSON.stringify(out));
35235
35513
  }
35236
35514
 
35515
+ // src/file-roots.ts
35516
+ import { homedir as homedir2 } from "node:os";
35517
+ import { delimiter as delimiter2, join as join4 } from "node:path";
35518
+
35519
+ // src/attachment-root.ts
35520
+ import { tmpdir, userInfo } from "node:os";
35521
+ import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
35522
+ var ATTACHMENT_DOWNLOAD_ROOT = join3(tmpdir(), `gogcli-mcp-attachments-${userInfo().uid}`);
35523
+ var ATTACHMENT_TTL_MS = 24 * 60 * 60 * 1e3;
35524
+
35525
+ // src/file-roots.ts
35526
+ var FILE_ROOTS_ENV = "GOG_FILE_ROOTS";
35527
+ function defaultFileRoot() {
35528
+ return join4(homedir2(), "gogcli-mcp-files");
35529
+ }
35530
+ function fileRoots() {
35531
+ const raw = readEnvVar(FILE_ROOTS_ENV);
35532
+ const roots = (raw ?? "").split(delimiter2).map((r) => r.trim()).filter(Boolean);
35533
+ return roots.length > 0 ? roots : [defaultFileRoot()];
35534
+ }
35535
+ function confinePath(path, param, opts = {}) {
35536
+ if (opts.allowDash && path === "-") return path;
35537
+ const roots = [...fileRoots(), ATTACHMENT_DOWNLOAD_ROOT];
35538
+ try {
35539
+ assertPathWithinRoots(path, roots);
35540
+ } catch {
35541
+ throw new Error(
35542
+ `${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}.`
35543
+ );
35544
+ }
35545
+ return path;
35546
+ }
35547
+ function confinePaths(paths, param) {
35548
+ for (const path of paths ?? []) confinePath(path, param);
35549
+ }
35550
+ function confineAtFile(value, param) {
35551
+ if (value.startsWith("@")) confinePath(value.slice(1), param);
35552
+ return value;
35553
+ }
35554
+
35555
+ // src/run-path-guard.ts
35556
+ var POSITIONAL_PATH_SUBCOMMANDS = {
35557
+ appscript: { pull: "gog_appscript_pull" },
35558
+ drive: { upload: "gog_drive_upload", sync: "gog_drive_sync_push" },
35559
+ gmail: { import: "gog_gmail_import" },
35560
+ slides: {
35561
+ "add-slide": "gog_slides_add_slide",
35562
+ "insert-image": "gog_slides_insert_image",
35563
+ "replace-slide": "gog_slides_replace_slide"
35564
+ }
35565
+ };
35566
+ var PATH_FLAGS = /* @__PURE__ */ new Set(["out", "output", "out-dir", "output-dir", "dir", "worker-dir", "attach", "file", "key", "cert", "replacements"]);
35567
+ var ID_FLAGS = {
35568
+ drive: /* @__PURE__ */ new Set(["file", "filter-file"])
35569
+ };
35570
+ var EXEC_FLAGS = /* @__PURE__ */ new Set(["on-change", "on-new", "mmdc"]);
35571
+ var SHORT_PATH_CLUSTER = /^-(?!-)[A-Za-z]*[of]/;
35572
+ var LONG_FLAG = /^--([A-Za-z0-9][A-Za-z0-9-]*)(?:=([\s\S]*))?$/;
35573
+ function isPathFlag(service, name) {
35574
+ if (ID_FLAGS[service]?.has(name)) return false;
35575
+ return PATH_FLAGS.has(name) || name.endsWith("-file");
35576
+ }
35577
+ function confineFlagValue(flag, value) {
35578
+ if (value === "-") return;
35579
+ confinePath(value, flag);
35580
+ for (const part of value.split(",")) {
35581
+ if (part !== "" && part !== "-") confinePath(part, flag);
35582
+ }
35583
+ }
35584
+ function positionalPathTool(service, subcommand) {
35585
+ return POSITIONAL_PATH_SUBCOMMANDS[service]?.[subcommand];
35586
+ }
35587
+ function assertRunPathsConfined(service, subcommand, args) {
35588
+ const dedicated = positionalPathTool(service, subcommand);
35589
+ if (dedicated) {
35590
+ throw new Error(
35591
+ `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.`
35592
+ );
35593
+ }
35594
+ for (let i = 0; i < args.length; i++) {
35595
+ const arg = args[i];
35596
+ if (SHORT_PATH_CLUSTER.test(arg)) {
35597
+ throw new Error(
35598
+ `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.`
35599
+ );
35600
+ }
35601
+ const match = LONG_FLAG.exec(arg);
35602
+ if (!match) continue;
35603
+ const name = match[1].toLowerCase();
35604
+ if (EXEC_FLAGS.has(name)) {
35605
+ throw new Error(`The flag --${name} is not allowed here: it runs a local program chosen by the caller.`);
35606
+ }
35607
+ const isJson = name.endsWith("-json");
35608
+ if (!isJson && !isPathFlag(service, name)) continue;
35609
+ let value = match[2];
35610
+ if (value === void 0) {
35611
+ value = args[i + 1];
35612
+ if (value === void 0) continue;
35613
+ i++;
35614
+ }
35615
+ if (isJson) {
35616
+ if (value.startsWith("@")) confineFlagValue(`--${name}`, value.slice(1));
35617
+ continue;
35618
+ }
35619
+ confineFlagValue(`--${name}`, value);
35620
+ }
35621
+ }
35622
+
35237
35623
  // src/tools/utils.ts
35238
35624
  var PAYLOAD_INLINE_MAX = 4096;
35239
35625
  function payloadArg(inlineFlag, fileFlag, value, ext) {
@@ -35291,9 +35677,17 @@ function pushPaginationFlags(args, p) {
35291
35677
  if (p.all) args.push("--all");
35292
35678
  }
35293
35679
  function registerRunTool(server, options) {
35294
- const { service, examples, omitAccount = false, note } = options;
35680
+ const { service, examples, omitAccount = false, note, allowedSubcommands, vet, gmailNoSend = false } = options;
35681
+ for (const [, example] of examples.matchAll(/"([^"]+)"/g)) {
35682
+ const refused = allowedSubcommands && !allowedSubcommands.includes(example) || vet?.(example, []) || positionalPathTool(service, example);
35683
+ if (refused) {
35684
+ throw new Error(`gog_${service}_run example "${example}" is refused by the tool itself; list a subcommand it runs.`);
35685
+ }
35686
+ }
35295
35687
  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.`;
35296
- const description = note ? `${baseDescription} ${note}` : baseDescription;
35688
+ const restriction = allowedSubcommands ? ` Only these subcommands are available: ${allowedSubcommands.join(", ")}.` : "";
35689
+ 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.';
35690
+ const description = `${baseDescription}${restriction}${safety}${note ? ` ${note}` : ""}`;
35297
35691
  const inputSchema = {
35298
35692
  subcommand: external_exports.string().describe(`The gog ${service} subcommand to run, e.g. ${examples}`),
35299
35693
  args: external_exports.array(external_exports.string()).describe("Additional positional args and flags")
@@ -35307,7 +35701,21 @@ function registerRunTool(server, options) {
35307
35701
  inputSchema: external_exports.object(inputSchema)
35308
35702
  }, async (rawArgs) => {
35309
35703
  const { subcommand, args, account } = rawArgs;
35310
- return runOrDiagnose([service, subcommand, ...args], { account });
35704
+ try {
35705
+ assertSafeSubcommand(subcommand);
35706
+ assertSafeForwardedArgs(args);
35707
+ if (allowedSubcommands && !allowedSubcommands.includes(subcommand)) {
35708
+ throw new Error(
35709
+ `gog ${service} ${subcommand} is not available through gog_${service}_run. Allowed: ${allowedSubcommands.join(", ")}.`
35710
+ );
35711
+ }
35712
+ const refusal = vet?.(subcommand, args);
35713
+ if (refusal) throw new Error(refusal);
35714
+ assertRunPathsConfined(service, subcommand, args);
35715
+ } catch (err) {
35716
+ return errorResult(errorText(err));
35717
+ }
35718
+ return runOrDiagnose([service, subcommand, ...args], gmailNoSend ? { account, gmailNoSend } : { account });
35311
35719
  });
35312
35720
  }
35313
35721
  function errorText(err) {
@@ -35435,7 +35843,240 @@ function assertNotBoth(inlineParam, fileParam, inlineValue, fileValue) {
35435
35843
  }
35436
35844
  }
35437
35845
 
35846
+ // src/dispatch-confirmation.ts
35847
+ import { createHash as createHash3 } from "node:crypto";
35848
+ import { readFileSync } from "node:fs";
35849
+
35850
+ // src/send-confirm-token.ts
35851
+ import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
35852
+ var CONFIRM_TOKEN_TTL_DEFAULT_SECONDS = 600;
35853
+ var PREFIX = "gct1";
35854
+ var secret;
35855
+ var spent = /* @__PURE__ */ new Map();
35856
+ function sendConfirmFallbackEnabled() {
35857
+ return readEnvVar("GOG_SEND_CONFIRM_FALLBACK")?.trim().toLowerCase() === "token";
35858
+ }
35859
+ function confirmTokenTtlSeconds() {
35860
+ const raw = readEnvVar("GOG_CONFIRM_TTL_SECONDS")?.trim();
35861
+ if (raw && /^\d+$/.test(raw)) {
35862
+ const n = Number(raw);
35863
+ if (n > 0) return n;
35864
+ }
35865
+ return CONFIRM_TOKEN_TTL_DEFAULT_SECONDS;
35866
+ }
35867
+ function hmacKey() {
35868
+ if (!secret) {
35869
+ const configured = readEnvVar("GOG_CONFIRM_SECRET");
35870
+ secret = configured ? Buffer.from(configured, "utf8") : randomBytes(32);
35871
+ }
35872
+ return secret;
35873
+ }
35874
+ function canonicalize(value) {
35875
+ if (Array.isArray(value)) return value.map(canonicalize);
35876
+ if (value && typeof value === "object") {
35877
+ const out = {};
35878
+ for (const key of Object.keys(value).sort()) {
35879
+ const v = value[key];
35880
+ if (v !== void 0) out[key] = canonicalize(v);
35881
+ }
35882
+ return out;
35883
+ }
35884
+ return value;
35885
+ }
35886
+ function hashSendPayload(payload) {
35887
+ return createHash2("sha256").update(JSON.stringify(canonicalize(payload))).digest("hex");
35888
+ }
35889
+ function sign(body) {
35890
+ return createHmac2("sha256", hmacKey()).update(`${PREFIX}.${body}`).digest("base64url");
35891
+ }
35892
+ function issueConfirmToken(binding, now = Date.now()) {
35893
+ const exp = now + confirmTokenTtlSeconds() * 1e3;
35894
+ const claims = {
35895
+ t: binding.tool,
35896
+ a: binding.account,
35897
+ g: binding.target,
35898
+ ...binding.revision === void 0 ? {} : { r: binding.revision },
35899
+ h: binding.payloadHash,
35900
+ iat: now,
35901
+ exp,
35902
+ n: randomBytes(16).toString("base64url")
35903
+ };
35904
+ const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
35905
+ return { token: `${PREFIX}.${body}.${sign(body)}`, expiresAt: new Date(exp).toISOString() };
35906
+ }
35907
+ function signatureMatches(body, sig) {
35908
+ const expected = Buffer.from(sign(body), "utf8");
35909
+ const actual = Buffer.from(sig, "utf8");
35910
+ return expected.length === actual.length && timingSafeEqual2(expected, actual);
35911
+ }
35912
+ function parseClaims(body) {
35913
+ try {
35914
+ return JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
35915
+ } catch {
35916
+ return void 0;
35917
+ }
35918
+ }
35919
+ function verifyConfirmToken(token, binding, now = Date.now()) {
35920
+ for (const [nonce, exp] of spent) if (exp < now) spent.delete(nonce);
35921
+ const parts = token.split(".");
35922
+ if (parts.length !== 3 || parts[0] !== PREFIX || !parts[1] || !parts[2]) return { ok: false, error: "TOKEN_INVALID" };
35923
+ const [, body, sig] = parts;
35924
+ if (!signatureMatches(body, sig)) return { ok: false, error: "TOKEN_INVALID" };
35925
+ const claims = parseClaims(body);
35926
+ if (!claims) return { ok: false, error: "TOKEN_INVALID" };
35927
+ if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
35928
+ return { ok: false, error: "TOKEN_INVALID" };
35929
+ }
35930
+ if (spent.has(claims.n)) return { ok: false, error: "TOKEN_REUSED" };
35931
+ if (now > claims.exp) return { ok: false, error: "TOKEN_EXPIRED" };
35932
+ if (claims.r !== binding.revision) return { ok: false, error: "DRAFT_CHANGED", reason: "message-id-rotated" };
35933
+ if (claims.h !== binding.payloadHash) return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
35934
+ spent.set(claims.n, claims.exp);
35935
+ return { ok: true };
35936
+ }
35937
+
35938
+ // src/dispatch-confirmation.ts
35939
+ var BODY_PREVIEW_MAX = 2048;
35940
+ function bodyPreview(text) {
35941
+ if (!text) return void 0;
35942
+ if (text.length <= BODY_PREVIEW_MAX) return text;
35943
+ return `${text.slice(0, BODY_PREVIEW_MAX)}\u2026 [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
35944
+ }
35945
+ function attachmentNames(paths, inline) {
35946
+ return [...paths ?? [], ...(inline ?? []).map((a) => a.filename)];
35947
+ }
35948
+ function attachmentDetails(paths, inline) {
35949
+ const out = [];
35950
+ for (const path of paths ?? []) {
35951
+ let bytes;
35952
+ try {
35953
+ bytes = readFileSync(path);
35954
+ } catch {
35955
+ bytes = void 0;
35956
+ }
35957
+ out.push(bytes ? { name: path, size: bytes.length, sha256: createHash3("sha256").update(bytes).digest("hex") } : { name: path, size: null });
35958
+ }
35959
+ for (const a of inline ?? []) {
35960
+ out.push({
35961
+ name: a.filename,
35962
+ size: Buffer.from(a.contentBase64, "base64").length,
35963
+ sha256: createHash3("sha256").update(a.contentBase64).digest("hex")
35964
+ });
35965
+ }
35966
+ return out;
35967
+ }
35968
+ function senderPreview(account, from) {
35969
+ return from ?? account ?? readEnvVar("GOG_ACCOUNT") ?? "the gog account's default address";
35970
+ }
35971
+ function attachmentPreview(details) {
35972
+ return details.map(({ name, size }) => ({ name, size }));
35973
+ }
35974
+ 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.";
35975
+ 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.";
35976
+ var FALLBACK_HINT = "Or set GOG_SEND_CONFIRM_FALLBACK=token to enable two-step confirmation.";
35977
+ 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.';
35978
+ var confirmTokenParam = external_exports.string().optional().describe(
35979
+ `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.`
35980
+ );
35981
+ var TOKEN_ERROR_NOTE = {
35982
+ 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.",
35983
+ 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.",
35984
+ 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."
35985
+ };
35986
+ var DRAFT_CHANGED_NOTE = {
35987
+ "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.",
35988
+ "payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
35989
+ };
35990
+ function isToolResult(value) {
35991
+ return Array.isArray(value.content);
35992
+ }
35993
+ function rejection2(data) {
35994
+ return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
35995
+ }
35996
+ async function tokenConfirmation(op, fallback) {
35997
+ const subject = await fallback.subject();
35998
+ if (isToolResult(subject)) return subject;
35999
+ const binding = {
36000
+ tool: fallback.tool,
36001
+ account: fallback.account ?? readEnvVar("GOG_ACCOUNT") ?? "",
36002
+ target: subject.target,
36003
+ ...subject.revision === void 0 ? {} : { revision: subject.revision },
36004
+ payloadHash: hashSendPayload(subject.payload)
36005
+ };
36006
+ const phaseOne = () => {
36007
+ const { token, expiresAt } = issueConfirmToken(binding);
36008
+ return {
36009
+ action: op,
36010
+ preview: subject.preview,
36011
+ confirmToken: token,
36012
+ expiresAt,
36013
+ ttlSeconds: confirmTokenTtlSeconds(),
36014
+ instruction: fallback.instruction ?? CONFIRM_ACTION_INSTRUCTION
36015
+ };
36016
+ };
36017
+ if (!fallback.confirmToken) {
36018
+ return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
36019
+ }
36020
+ const verdict = verifyConfirmToken(fallback.confirmToken, binding);
36021
+ if (verdict.ok) return void 0;
36022
+ if (verdict.error === "DRAFT_CHANGED") {
36023
+ return rejection2({
36024
+ error: "DRAFT_CHANGED",
36025
+ reason: verdict.reason,
36026
+ note: `${DRAFT_CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
36027
+ ...phaseOne()
36028
+ });
36029
+ }
36030
+ return rejection2({ error: verdict.error, action: op, note: TOKEN_ERROR_NOTE[verdict.error] });
36031
+ }
36032
+ function gatedElsewhere(what, via, does, tool) {
36033
+ return `${what} ${does} and is not available through ${via}. Use ${tool}, which asks the user to confirm.`;
36034
+ }
36035
+ function hasCommandWord(args, words) {
36036
+ return args.find((a) => words.has(a.toLowerCase()));
36037
+ }
36038
+ async function requireDispatchConfirmation(ctx, options) {
36039
+ const { action, fallback } = options;
36040
+ if (fallback && callerAcceptsFormElicitation(ctx) === false && sendConfirmFallbackEnabled()) {
36041
+ return tokenConfirmation(action, fallback);
36042
+ }
36043
+ const note = fallback ? [options.unsupportedNote, FALLBACK_HINT].filter(Boolean).join(" ") : options.unsupportedNote;
36044
+ return requireConfirmation(ctx, {
36045
+ action,
36046
+ message: options.message,
36047
+ details: options.details,
36048
+ confirmationLabel: options.confirmationLabel,
36049
+ ...note ? { unsupportedNote: note } : {}
36050
+ });
36051
+ }
36052
+ function resultText(result) {
36053
+ const first = result.content[0];
36054
+ return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
36055
+ }
36056
+
35438
36057
  // src/tools/api.ts
36058
+ var GMAIL_API_BLOCKED = /(?:\.send$|forwarding|filters\.create|filters\.update|delegates\.create)/i;
36059
+ var DISPATCH_API_BLOCKED = {
36060
+ chat: [{ method: /(?:^|\.)spaces\.messages\.create$/i, does: "posts a Chat message", tool: "gog_chat_messages_send / gog_chat_dm_send" }],
36061
+ drive: [{ method: /(?:^|\.)permissions\.(?:create|update)$/i, does: "grants access to a file", tool: "gog_drive_share" }],
36062
+ classroom: [
36063
+ { method: /(?:^|\.)courses\.announcements\.create$/i, does: "posts to a class", tool: "gog_classroom_announcements_create" },
36064
+ { method: /(?:^|\.)invitations\.create$/i, does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
36065
+ ],
36066
+ calendar: [
36067
+ { method: /(?:^|\.)events\.(?:insert|import|quickadd)$/i, does: "can put an event on guests' calendars", tool: "gog_calendar_create" },
36068
+ { method: /(?:^|\.)events\.(?:update|patch)$/i, does: "can change what guests see", tool: "gog_calendar_update / gog_calendar_respond" }
36069
+ ]
36070
+ };
36071
+ function refusedApiCall(api, method) {
36072
+ const name = api.trim().toLowerCase();
36073
+ const m = method.trim();
36074
+ if (name === "gmail" && GMAIL_API_BLOCKED.test(m)) {
36075
+ 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.`;
36076
+ }
36077
+ const hit = (Object.hasOwn(DISPATCH_API_BLOCKED, name) ? DISPATCH_API_BLOCKED[name] : []).find((b) => b.method.test(m));
36078
+ return hit ? gatedElsewhere(`${name} ${m}`, "gog_api_call", hit.does, hit.tool) : void 0;
36079
+ }
35439
36080
  function registerApiTools(server) {
35440
36081
  server.registerTool("gog_api_list", {
35441
36082
  description: "List the Google Discovery APIs available for gog_api_call / gog_api_describe (name + version + title).",
@@ -35459,12 +36100,12 @@ function registerApiTools(server) {
35459
36100
  account: accountParam
35460
36101
  })
35461
36102
  }, async ({ api, version: version2, method, account }) => {
35462
- const args = ["api", "describe", api, version2];
35463
- if (method) args.push(method);
36103
+ const args = ["api", "describe", pos(api), pos(version2)];
36104
+ if (method) args.push(pos(method));
35464
36105
  return runOrDiagnose(args, { account });
35465
36106
  });
35466
36107
  server.registerTool("gog_api_call", {
35467
- 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.",
36108
+ 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.",
35468
36109
  annotations: { destructiveHint: true },
35469
36110
  inputSchema: external_exports.object({
35470
36111
  api: external_exports.string().describe("Discovery API name (e.g. drive, gmail, calendar)"),
@@ -35478,14 +36119,23 @@ function registerApiTools(server) {
35478
36119
  account: accountParam
35479
36120
  })
35480
36121
  }, async ({ api, version: version2, method, params, body, scope, allowWrite, dryRun, account }) => {
35481
- const args = ["api", "call", api, version2, method];
36122
+ try {
36123
+ assertSafeForwardedArgs([api, version2, method]);
36124
+ const refusal = refusedApiCall(api, method);
36125
+ if (refusal) throw new Error(refusal);
36126
+ if (body) confineAtFile(body, "body");
36127
+ if (params) confineAtFile(params, "params");
36128
+ } catch (err) {
36129
+ return errorResult(errorText(err));
36130
+ }
36131
+ const args = ["api", "call", pos(api), pos(version2), pos(method)];
35482
36132
  if (params) args.push(`--params=${params}`);
35483
36133
  if (body) args.push(`--body=${body}`);
35484
36134
  if (scope) args.push(`--scope=${scope}`);
35485
36135
  if (allowWrite) args.push("--allow-write");
35486
36136
  if (dryRun) args.push("--dry-run");
35487
36137
  if (allowWrite) args.push("--force");
35488
- return runOrDiagnose(args, { account });
36138
+ return runOrDiagnose(args, { account, gmailNoSend: true });
35489
36139
  });
35490
36140
  }
35491
36141
 
@@ -35503,7 +36153,7 @@ function registerAppScriptTools(server) {
35503
36153
  account: accountParam
35504
36154
  })
35505
36155
  }, async ({ scriptId, account }) => {
35506
- return runOrDiagnose(["appscript", "get", scriptId], { account });
36156
+ return runOrDiagnose(["appscript", "get", pos(scriptId)], { account });
35507
36157
  });
35508
36158
  server.registerTool("gog_appscript_content", {
35509
36159
  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,
@@ -35513,19 +36163,20 @@ function registerAppScriptTools(server) {
35513
36163
  account: accountParam
35514
36164
  })
35515
36165
  }, async ({ scriptId, account }) => {
35516
- return runOrDiagnose(["appscript", "content", scriptId], { account });
36166
+ return runOrDiagnose(["appscript", "content", pos(scriptId)], { account });
35517
36167
  });
35518
36168
  server.registerTool("gog_appscript_pull", {
35519
36169
  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,
35520
36170
  annotations: { destructiveHint: true },
35521
36171
  inputSchema: external_exports.object({
35522
36172
  scriptId: scriptIdParam,
35523
- dir: external_exports.string().describe("Destination directory, resolved on the machine where gog runs"),
36173
+ dir: external_exports.string().describe("Destination directory, resolved on the machine where gog runs; must be inside the server's GOG_FILE_ROOTS directories"),
35524
36174
  overwrite: external_exports.boolean().optional().describe("Overwrite files that already exist in dir"),
35525
36175
  account: accountParam
35526
36176
  })
35527
36177
  }, async ({ scriptId, dir, overwrite, account }) => {
35528
- const args = ["appscript", "pull", scriptId, dir];
36178
+ confinePath(dir, "dir");
36179
+ const args = ["appscript", "pull", pos(scriptId), pos(dir)];
35529
36180
  if (overwrite) args.push("--overwrite");
35530
36181
  return runOrDiagnose(args, { account });
35531
36182
  });
@@ -35551,7 +36202,7 @@ function registerAppScriptTools(server) {
35551
36202
  account: accountParam
35552
36203
  })
35553
36204
  }, async ({ scriptId, max, pageToken, page, all, account }) => {
35554
- const args = ["appscript", "deployments", scriptId];
36205
+ const args = ["appscript", "deployments", pos(scriptId)];
35555
36206
  pushPaginationFlags(args, { max, pageToken, page, all });
35556
36207
  return runOrDiagnose(args, { account });
35557
36208
  });
@@ -35564,7 +36215,7 @@ function registerAppScriptTools(server) {
35564
36215
  account: accountParam
35565
36216
  })
35566
36217
  }, async ({ scriptId, max, pageToken, page, all, account }) => {
35567
- const args = ["appscript", "versions", scriptId];
36218
+ const args = ["appscript", "versions", pos(scriptId)];
35568
36219
  pushPaginationFlags(args, { max, pageToken, page, all });
35569
36220
  return runOrDiagnose(args, { account });
35570
36221
  });
@@ -35590,7 +36241,7 @@ function registerAppScriptTools(server) {
35590
36241
  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}`);
35591
36242
  }
35592
36243
  }
35593
- const args = ["appscript", "run", scriptId, functionName];
36244
+ const args = ["appscript", "run", pos(scriptId), pos(functionName)];
35594
36245
  if (params !== void 0) args.push(`--params=${params}`);
35595
36246
  if (devMode) args.push("--dev-mode");
35596
36247
  return runOrDiagnose(args, { account });
@@ -35603,6 +36254,7 @@ function registerAppScriptTools(server) {
35603
36254
  }
35604
36255
 
35605
36256
  // src/tools/auth.ts
36257
+ var AUTH_RUN_SUBCOMMANDS = ["list", "status", "services", "remove", "alias"];
35606
36258
  function registerAuthToolsWith(server, defaultServices) {
35607
36259
  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.`;
35608
36260
  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.";
@@ -35660,7 +36312,7 @@ function registerAuthToolsWith(server, defaultServices) {
35660
36312
  })
35661
36313
  }, async ({ email: email3, services = defaultServices, extraScopes }) => {
35662
36314
  try {
35663
- const args = ["auth", "add", email3, "--services", services];
36315
+ const args = ["auth", "add", pos(email3), "--services", services];
35664
36316
  if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, "--force-consent");
35665
36317
  return rawTextResult(await run(args, {
35666
36318
  interactive: true,
@@ -35680,7 +36332,7 @@ function registerAuthToolsWith(server, defaultServices) {
35680
36332
  })
35681
36333
  }, async ({ email: email3, services = defaultServices, extraScopes }) => {
35682
36334
  try {
35683
- const args = ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"];
36335
+ const args = ["auth", "add", pos(email3), "--remote", "--step", "1", "--services", services, "--force-consent"];
35684
36336
  if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
35685
36337
  return rawTextResult(await run(args, { redactMode: "tokens" }));
35686
36338
  } catch (err) {
@@ -35707,7 +36359,7 @@ function registerAuthToolsWith(server, defaultServices) {
35707
36359
  const args = [
35708
36360
  "auth",
35709
36361
  "add",
35710
- email3,
36362
+ pos(email3),
35711
36363
  "--remote",
35712
36364
  "--step",
35713
36365
  "2",
@@ -35727,6 +36379,7 @@ function registerAuthToolsWith(server, defaultServices) {
35727
36379
  service: "auth",
35728
36380
  examples: '"remove", "alias", "list"',
35729
36381
  omitAccount: true,
36382
+ allowedSubcommands: AUTH_RUN_SUBCOMMANDS,
35730
36383
  note: "For browser-based authorization, use gog_auth_add instead."
35731
36384
  });
35732
36385
  }
@@ -35735,6 +36388,54 @@ function registerAuthTools(server) {
35735
36388
  }
35736
36389
 
35737
36390
  // src/tools/calendar.ts
36391
+ var CALENDAR_GATED = {
36392
+ create: "gog_calendar_create",
36393
+ add: "gog_calendar_create",
36394
+ new: "gog_calendar_create",
36395
+ update: "gog_calendar_update",
36396
+ edit: "gog_calendar_update",
36397
+ set: "gog_calendar_update",
36398
+ respond: "gog_calendar_respond",
36399
+ rsvp: "gog_calendar_respond",
36400
+ reply: "gog_calendar_respond"
36401
+ };
36402
+ function vetCalendarRun(subcommand, _args) {
36403
+ const sub = subcommand.toLowerCase();
36404
+ const tool = Object.hasOwn(CALENDAR_GATED, sub) ? CALENDAR_GATED[sub] : void 0;
36405
+ return tool ? gatedElsewhere(`gog calendar ${sub}`, "gog_calendar_run", "can change what guests see", tool) : void 0;
36406
+ }
36407
+ function parseAttendees(value) {
36408
+ if (!value) return [];
36409
+ 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);
36410
+ }
36411
+ function timeOf(t) {
36412
+ const v = t?.dateTime ?? t?.date;
36413
+ return typeof v === "string" ? v : void 0;
36414
+ }
36415
+ function eventSnapshot(raw) {
36416
+ let event;
36417
+ try {
36418
+ event = JSON.parse(raw)?.event;
36419
+ } catch {
36420
+ event = void 0;
36421
+ }
36422
+ const str = (v) => typeof v === "string" ? v : void 0;
36423
+ return {
36424
+ summary: str(event?.summary),
36425
+ start: timeOf(event?.start),
36426
+ end: timeOf(event?.end),
36427
+ organizer: str(event?.organizer?.email),
36428
+ // Guests are the OTHER people: not this account's own attendee row, not rooms.
36429
+ guests: (Array.isArray(event?.attendees) ? event.attendees : []).filter((a) => a.self !== true && a.resource !== true && typeof a.email === "string").map((a) => a.email),
36430
+ etag: str(event?.etag)
36431
+ };
36432
+ }
36433
+ async function readEvent(calendarId, eventId, account) {
36434
+ const got = await runOrDiagnose(["calendar", "event", pos(calendarId), pos(eventId)], { account });
36435
+ if (got.isError) return { error: got };
36436
+ return { event: eventSnapshot(resultText(got)) };
36437
+ }
36438
+ var CALENDAR_CONFIRM_LABEL = "Confirm that this change, which other people will see, should be made now.";
35738
36439
  var reminderParams = {
35739
36440
  reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
35740
36441
  `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.`
@@ -35788,7 +36489,7 @@ function registerCalendarTools(server) {
35788
36489
  })
35789
36490
  }, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, view, account }) => {
35790
36491
  const args = ["calendar", "events"];
35791
- if (calendarId) args.push(calendarId);
36492
+ if (calendarId) args.push(pos(calendarId));
35792
36493
  if (from) args.push(`--from=${from}`);
35793
36494
  if (to) args.push(`--to=${to}`);
35794
36495
  if (days !== void 0) args.push(`--days=${days}`);
@@ -35817,12 +36518,12 @@ function registerCalendarTools(server) {
35817
36518
  account: accountParam
35818
36519
  })
35819
36520
  }, async ({ calendarId, eventId, timezone, account }) => {
35820
- const args = ["calendar", "event", calendarId, eventId];
36521
+ const args = ["calendar", "event", pos(calendarId), pos(eventId)];
35821
36522
  if (timezone) args.push(`--timezone=${timezone}`);
35822
36523
  return runOrDiagnose(args, { account });
35823
36524
  });
35824
36525
  server.registerTool("gog_calendar_create", {
35825
- 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).",
36526
+ 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,
35826
36527
  annotations: { destructiveHint: false },
35827
36528
  inputSchema: external_exports.object({
35828
36529
  calendarId: external_exports.string().describe('Calendar ID (use "primary" for the default calendar)'),
@@ -35836,10 +36537,11 @@ function registerCalendarTools(server) {
35836
36537
  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."),
35837
36538
  withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)"),
35838
36539
  ...reminderParams,
35839
- account: accountParam
36540
+ account: accountParam,
36541
+ confirmToken: confirmTokenParam
35840
36542
  })
35841
- }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
35842
- const args = ["calendar", "create", calendarId, `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
36543
+ }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account, confirmToken }, ctx) => {
36544
+ const args = ["calendar", "create", pos(calendarId), `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
35843
36545
  if (description) args.push(`--description=${description}`);
35844
36546
  if (location) args.push(`--location=${location}`);
35845
36547
  if (attendees) args.push(`--attendees=${attendees}`);
@@ -35847,10 +36549,28 @@ function registerCalendarTools(server) {
35847
36549
  if (timezone) args.push(`--timezone=${timezone}`);
35848
36550
  if (withZoom) args.push("--with-zoom");
35849
36551
  pushReminderFlags(args, { reminders, noReminders });
36552
+ const guests = parseAttendees(attendees);
36553
+ if (guests.length > 0) {
36554
+ const event = { calendarId, summary, from, to, allDay: Boolean(allDay), timezone, location, description, guests, attendees, withZoom: Boolean(withZoom) };
36555
+ const confirmation = await requireDispatchConfirmation(ctx, {
36556
+ action: "calendar.create",
36557
+ message: "Review and confirm this event, which will appear on the guests' calendars:",
36558
+ confirmationLabel: CALENDAR_CONFIRM_LABEL,
36559
+ details: event,
36560
+ unsupportedNote: "Create it without attendees instead; the user can add the guests in Google Calendar.",
36561
+ fallback: {
36562
+ tool: "gog_calendar_create",
36563
+ account,
36564
+ confirmToken,
36565
+ subject: () => ({ target: calendarId, payload: event, preview: event })
36566
+ }
36567
+ });
36568
+ if (confirmation) return confirmation;
36569
+ }
35850
36570
  return runOrDiagnose(args, { account });
35851
36571
  });
35852
36572
  server.registerTool("gog_calendar_update", {
35853
- 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.",
36573
+ 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,
35854
36574
  annotations: { destructiveHint: false },
35855
36575
  inputSchema: external_exports.object({
35856
36576
  calendarId: external_exports.string().describe("Calendar ID"),
@@ -35868,10 +36588,11 @@ function registerCalendarTools(server) {
35868
36588
  removeZoom: external_exports.boolean().optional().describe("Remove the event's Zoom video conference"),
35869
36589
  removeMeet: external_exports.boolean().optional().describe("Remove the event's Google Meet video conference (clears conference data only)"),
35870
36590
  ...reminderParams,
35871
- account: accountParam
36591
+ account: accountParam,
36592
+ confirmToken: confirmTokenParam
35872
36593
  })
35873
- }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
35874
- const args = ["calendar", "update", calendarId, eventId];
36594
+ }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account, confirmToken }, ctx) => {
36595
+ const args = ["calendar", "update", pos(calendarId), pos(eventId)];
35875
36596
  if (summary !== void 0) args.push(`--summary=${summary}`);
35876
36597
  if (from !== void 0) args.push(`--from=${from}`);
35877
36598
  if (to !== void 0) args.push(`--to=${to}`);
@@ -35885,6 +36606,45 @@ function registerCalendarTools(server) {
35885
36606
  if (removeZoom) args.push("--remove-zoom");
35886
36607
  if (removeMeet) args.push("--remove-meet");
35887
36608
  pushReminderFlags(args, { reminders, noReminders });
36609
+ const changes = Object.fromEntries(Object.entries({
36610
+ summary,
36611
+ from,
36612
+ to,
36613
+ description,
36614
+ location,
36615
+ attendees,
36616
+ addAttendees,
36617
+ attachments,
36618
+ withZoom,
36619
+ regenerateZoom,
36620
+ removeZoom,
36621
+ removeMeet
36622
+ }).filter(([, v]) => v !== void 0 && v !== false));
36623
+ if (Object.keys(changes).length > 0) {
36624
+ const read = await readEvent(calendarId, eventId, account);
36625
+ if (read.error) return read.error;
36626
+ const { etag, ...current } = read.event;
36627
+ const added = [...parseAttendees(attendees), ...parseAttendees(addAttendees)];
36628
+ if (current.guests.length > 0 || added.length > 0) {
36629
+ const view = { calendarId, eventId, current, changes };
36630
+ const confirmation = await requireDispatchConfirmation(ctx, {
36631
+ action: "calendar.update",
36632
+ message: "Review and confirm this change to an event other people are on:",
36633
+ confirmationLabel: CALENDAR_CONFIRM_LABEL,
36634
+ details: view,
36635
+ unsupportedNote: "Ask the user to make this change in Google Calendar.",
36636
+ fallback: {
36637
+ tool: "gog_calendar_update",
36638
+ account,
36639
+ confirmToken,
36640
+ // The etag rotates on any edit, so an event changed elsewhere between
36641
+ // the two phases is DRAFT_CHANGED rather than edited over.
36642
+ subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
36643
+ }
36644
+ });
36645
+ if (confirmation) return confirmation;
36646
+ }
36647
+ }
35888
36648
  return runOrDiagnose(args, { account });
35889
36649
  });
35890
36650
  server.registerTool("gog_calendar_delete", {
@@ -35896,24 +36656,43 @@ function registerCalendarTools(server) {
35896
36656
  account: accountParam
35897
36657
  })
35898
36658
  }, async ({ calendarId, eventId, account }) => {
35899
- return runOrDiagnose(["calendar", "delete", calendarId, eventId, "--force"], { account });
36659
+ return runOrDiagnose(["calendar", "delete", pos(calendarId), pos(eventId), "--force"], { account });
35900
36660
  });
35901
36661
  server.registerTool("gog_calendar_respond", {
35902
- description: "Respond to a calendar event invitation.",
36662
+ 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,
35903
36663
  annotations: { destructiveHint: true },
35904
36664
  inputSchema: external_exports.object({
35905
36665
  calendarId: external_exports.string().describe("Calendar ID"),
35906
36666
  eventId: external_exports.string().describe("Event ID"),
35907
36667
  status: external_exports.enum(["accepted", "declined", "tentative"]).describe("Response status"),
35908
36668
  comment: external_exports.string().optional().describe("Optional comment to include with response"),
35909
- account: accountParam
36669
+ account: accountParam,
36670
+ confirmToken: confirmTokenParam
35910
36671
  })
35911
- }, async ({ calendarId, eventId, status, comment, account }) => {
35912
- const args = ["calendar", "respond", calendarId, eventId, `--status=${status}`];
36672
+ }, async ({ calendarId, eventId, status, comment, account, confirmToken }, ctx) => {
36673
+ const args = ["calendar", "respond", pos(calendarId), pos(eventId), `--status=${status}`];
35913
36674
  if (comment) args.push(`--comment=${comment}`);
36675
+ const read = await readEvent(calendarId, eventId, account);
36676
+ if (read.error) return read.error;
36677
+ const { etag, ...event } = read.event;
36678
+ const view = { calendarId, eventId, event, response: status, ...comment ? { comment } : {} };
36679
+ const confirmation = await requireDispatchConfirmation(ctx, {
36680
+ action: "calendar.respond",
36681
+ message: "Review and confirm this response, which the organizer will see:",
36682
+ confirmationLabel: "Confirm that this response should be recorded now.",
36683
+ details: view,
36684
+ unsupportedNote: "Ask the user to respond from Google Calendar.",
36685
+ fallback: {
36686
+ tool: "gog_calendar_respond",
36687
+ account,
36688
+ confirmToken,
36689
+ subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
36690
+ }
36691
+ });
36692
+ if (confirmation) return confirmation;
35914
36693
  return runOrDiagnose(args, { account });
35915
36694
  });
35916
- registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"' });
36695
+ registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"', vet: vetCalendarRun });
35917
36696
  }
35918
36697
 
35919
36698
  // src/attachments.ts
@@ -35924,6 +36703,7 @@ var MAX_REQUEST_PAYLOAD_WIRE_BYTES = REQUEST_MAX_BODY_BYTES - REQUEST_BODY_JSON_
35924
36703
  var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
35925
36704
  function wireBytesOf(arg) {
35926
36705
  if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
36706
+ if (arg.kind === "positional") return Buffer.byteLength(arg.value, "utf8");
35927
36707
  return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
35928
36708
  }
35929
36709
  var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
@@ -35996,6 +36776,23 @@ function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
35996
36776
  }
35997
36777
 
35998
36778
  // src/tools/chat.ts
36779
+ var CHAT_SEND_WORDS = /* @__PURE__ */ new Set(["send", "create", "post"]);
36780
+ function vetChatRun(subcommand, args) {
36781
+ const sub = subcommand.toLowerCase();
36782
+ if (sub !== "messages" && sub !== "dm") return void 0;
36783
+ const word = hasCommandWord(args, CHAT_SEND_WORDS);
36784
+ if (!word) return void 0;
36785
+ return gatedElsewhere(
36786
+ `gog chat ${sub} ${word.toLowerCase()}`,
36787
+ "gog_chat_run",
36788
+ "posts a message",
36789
+ sub === "dm" ? "gog_chat_dm_send" : "gog_chat_messages_send"
36790
+ );
36791
+ }
36792
+ var CHAT_CONFIRM = {
36793
+ message: "Review and confirm this Google Chat message:",
36794
+ confirmationLabel: "Confirm that this Chat message should be posted now."
36795
+ };
35999
36796
  function registerChatTools(server) {
36000
36797
  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.';
36001
36798
  const spaceParam = external_exports.string().describe(
@@ -36026,7 +36823,7 @@ function registerChatTools(server) {
36026
36823
  account: accountParam
36027
36824
  })
36028
36825
  }, async ({ displayName, exact, max, account }) => {
36029
- const args = ["chat", "spaces", "find", displayName];
36826
+ const args = ["chat", "spaces", "find", pos(displayName)];
36030
36827
  if (exact) args.push("--exact");
36031
36828
  if (max !== void 0) args.push(`--max=${max}`);
36032
36829
  return runOrDiagnose(args, { account });
@@ -36040,7 +36837,7 @@ function registerChatTools(server) {
36040
36837
  account: accountParam
36041
36838
  })
36042
36839
  }, async ({ displayName, members: members2, account }) => {
36043
- const args = ["chat", "spaces", "create", displayName];
36840
+ const args = ["chat", "spaces", "create", pos(displayName)];
36044
36841
  if (members2) for (const member of members2) args.push(`--member=${member}`);
36045
36842
  return runOrDiagnose(args, { account });
36046
36843
  });
@@ -36053,7 +36850,7 @@ function registerChatTools(server) {
36053
36850
  account: accountParam
36054
36851
  })
36055
36852
  }, async ({ space, max, pageToken, page, all, account }) => {
36056
- const args = ["chat", "threads", "list", space];
36853
+ const args = ["chat", "threads", "list", pos(space)];
36057
36854
  pushPaginationFlags(args, { max, pageToken, page, all });
36058
36855
  return runOrDiagnose(args, { account });
36059
36856
  });
@@ -36069,7 +36866,7 @@ function registerChatTools(server) {
36069
36866
  account: accountParam
36070
36867
  })
36071
36868
  }, async ({ space, thread, unread, order, max, pageToken, page, all, account }) => {
36072
- const args = ["chat", "messages", "list", space];
36869
+ const args = ["chat", "messages", "list", pos(space)];
36073
36870
  if (thread) args.push(`--thread=${thread}`);
36074
36871
  if (unread) args.push("--unread");
36075
36872
  if (order) args.push(`--order=${order}`);
@@ -36093,7 +36890,7 @@ function registerChatTools(server) {
36093
36890
  account: accountParam
36094
36891
  })
36095
36892
  }, async ({ query, order, view, markup, max, pageToken, page, all, account }) => {
36096
- const args = ["chat", "messages", "search", query];
36893
+ const args = ["chat", "messages", "search", pos(query)];
36097
36894
  if (order) args.push(`--order=${order}`);
36098
36895
  if (view) args.push(`--view=${view}`);
36099
36896
  if (markup) args.push(`--markup=${markup}`);
@@ -36101,41 +36898,79 @@ function registerChatTools(server) {
36101
36898
  return runOrDiagnose(args, { account });
36102
36899
  });
36103
36900
  server.registerTool("gog_chat_messages_send", {
36104
- 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,
36901
+ 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,
36105
36902
  annotations: { destructiveHint: true },
36106
36903
  inputSchema: external_exports.object({
36107
36904
  space: spaceParam,
36108
36905
  text: external_exports.string().optional().describe("Message text. Optional only when an attachment is supplied."),
36109
36906
  thread: threadParam,
36110
36907
  attach: external_exports.array(external_exports.string()).optional().describe(
36111
- "Attachment file paths, read WHERE GOG RUNS. On a hosted or remote deployment that is not your machine \u2014 use attachInline there instead."
36908
+ "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)."
36112
36909
  ),
36113
36910
  attachInline: attachInlineParam,
36114
- account: accountParam
36911
+ account: accountParam,
36912
+ confirmToken: confirmTokenParam
36115
36913
  })
36116
- }, async ({ space, text, thread, attach, attachInline, account }) => {
36914
+ }, async ({ space, text, thread, attach, attachInline, account, confirmToken }, ctx) => {
36915
+ confinePaths(attach, "attach");
36117
36916
  if (text === void 0 && !attach?.length && !attachInline?.length) {
36118
36917
  throw new Error("A Chat message needs text, an attachment, or both.");
36119
36918
  }
36120
- const args = ["chat", "messages", "send", space];
36919
+ const args = ["chat", "messages", "send", pos(space)];
36121
36920
  if (text !== void 0) args.push(`--text=${text}`);
36122
36921
  if (thread) args.push(`--thread=${thread}`);
36123
36922
  if (attach) for (const path of attach) args.push(`--attach=${path}`);
36124
36923
  args.push(...inlineAttachmentArgs("attach", attachInline, args));
36924
+ const confirmation = await requireDispatchConfirmation(ctx, {
36925
+ ...CHAT_CONFIRM,
36926
+ action: "chat.message-send",
36927
+ details: { space, thread, textPreview: bodyPreview(text), attachments: attachmentNames(attach, attachInline) },
36928
+ fallback: {
36929
+ tool: "gog_chat_messages_send",
36930
+ account,
36931
+ confirmToken,
36932
+ subject: () => {
36933
+ const attachments = attachmentDetails(attach, attachInline);
36934
+ const from = senderPreview(account);
36935
+ return {
36936
+ target: space,
36937
+ payload: { from, space, thread, text, attachments },
36938
+ preview: { from, space, thread, text, attachments: attachmentPreview(attachments) }
36939
+ };
36940
+ }
36941
+ }
36942
+ });
36943
+ if (confirmation) return confirmation;
36125
36944
  return runOrDiagnose(args, { account });
36126
36945
  });
36127
36946
  server.registerTool("gog_chat_dm_send", {
36128
- 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,
36947
+ 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,
36129
36948
  annotations: { destructiveHint: true },
36130
36949
  inputSchema: external_exports.object({
36131
36950
  email: external_exports.string().describe("Recipient email address"),
36132
36951
  text: external_exports.string().describe("Message text"),
36133
36952
  thread: threadParam,
36134
- account: accountParam
36953
+ account: accountParam,
36954
+ confirmToken: confirmTokenParam
36135
36955
  })
36136
- }, async ({ email: email3, text, thread, account }) => {
36137
- const args = ["chat", "dm", "send", email3, `--text=${text}`];
36956
+ }, async ({ email: email3, text, thread, account, confirmToken }, ctx) => {
36957
+ const args = ["chat", "dm", "send", pos(email3), `--text=${text}`];
36138
36958
  if (thread) args.push(`--thread=${thread}`);
36959
+ const confirmation = await requireDispatchConfirmation(ctx, {
36960
+ ...CHAT_CONFIRM,
36961
+ action: "chat.dm-send",
36962
+ details: { to: email3, thread, textPreview: bodyPreview(text) },
36963
+ fallback: {
36964
+ tool: "gog_chat_dm_send",
36965
+ account,
36966
+ confirmToken,
36967
+ subject: () => {
36968
+ const from = senderPreview(account);
36969
+ return { target: email3, payload: { from, to: email3, thread, text }, preview: { from, to: email3, thread, text } };
36970
+ }
36971
+ }
36972
+ });
36973
+ if (confirmation) return confirmation;
36139
36974
  return runOrDiagnose(args, { account });
36140
36975
  });
36141
36976
  server.registerTool("gog_chat_dm_space", {
@@ -36146,7 +36981,7 @@ function registerChatTools(server) {
36146
36981
  account: accountParam
36147
36982
  })
36148
36983
  }, async ({ email: email3, account }) => {
36149
- return runOrDiagnose(["chat", "dm", "space", email3], { account });
36984
+ return runOrDiagnose(["chat", "dm", "space", pos(email3)], { account });
36150
36985
  });
36151
36986
  server.registerTool("gog_chat_reactions_list", {
36152
36987
  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,
@@ -36158,7 +36993,7 @@ function registerChatTools(server) {
36158
36993
  account: accountParam
36159
36994
  })
36160
36995
  }, async ({ message, space, max, pageToken, page, all, account }) => {
36161
- const args = ["chat", "messages", "reactions", "list", message];
36996
+ const args = ["chat", "messages", "reactions", "list", pos(message)];
36162
36997
  if (space) args.push(`--space=${space}`);
36163
36998
  pushPaginationFlags(args, { max, pageToken, page, all });
36164
36999
  return runOrDiagnose(args, { account });
@@ -36173,7 +37008,7 @@ function registerChatTools(server) {
36173
37008
  account: accountParam
36174
37009
  })
36175
37010
  }, async ({ message, emoji: emoji3, space, account }) => {
36176
- const args = ["chat", "messages", "reactions", "create", message, emoji3];
37011
+ const args = ["chat", "messages", "reactions", "create", pos(message), pos(emoji3)];
36177
37012
  if (space) args.push(`--space=${space}`);
36178
37013
  return runOrDiagnose(args, { account });
36179
37014
  });
@@ -36185,16 +37020,50 @@ function registerChatTools(server) {
36185
37020
  account: accountParam
36186
37021
  })
36187
37022
  }, async ({ reaction, account }) => {
36188
- return runOrDiagnose(["chat", "messages", "reactions", "delete", reaction], { account });
37023
+ return runOrDiagnose(["chat", "messages", "reactions", "delete", pos(reaction)], { account });
36189
37024
  });
36190
37025
  registerRunTool(server, {
36191
37026
  service: "chat",
36192
37027
  examples: '"spaces", "messages", "dm"',
37028
+ vet: vetChatRun,
36193
37029
  note: "Google Chat has no API for consumer accounts: every chat subcommand fails on an @gmail.com account regardless of scopes."
36194
37030
  });
36195
37031
  }
36196
37032
 
36197
37033
  // src/tools/classroom.ts
37034
+ var CLASSROOM_CREATE_WORDS = /* @__PURE__ */ new Set(["create", "add", "new"]);
37035
+ var CLASSROOM_GATED = {
37036
+ announcements: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
37037
+ announcement: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
37038
+ ann: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
37039
+ invitations: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
37040
+ invitation: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
37041
+ invites: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
37042
+ };
37043
+ function vetClassroomRun(subcommand, args) {
37044
+ const sub = subcommand.toLowerCase();
37045
+ const gated = Object.hasOwn(CLASSROOM_GATED, sub) ? CLASSROOM_GATED[sub] : void 0;
37046
+ if (!gated) return void 0;
37047
+ const word = hasCommandWord(args, CLASSROOM_CREATE_WORDS);
37048
+ return word ? gatedElsewhere(`gog classroom ${sub} ${word.toLowerCase()}`, "gog_classroom_run", gated.does, gated.tool) : void 0;
37049
+ }
37050
+ async function readCourse(courseId, account, runner = runOrDiagnose) {
37051
+ const got = await runner(["classroom", "courses", "get", pos(courseId)], { account });
37052
+ if (got.isError) return { error: got };
37053
+ let course;
37054
+ try {
37055
+ course = JSON.parse(resultText(got))?.course;
37056
+ } catch {
37057
+ course = void 0;
37058
+ }
37059
+ return {
37060
+ course: {
37061
+ id: courseId,
37062
+ ...typeof course?.name === "string" ? { name: course.name } : {},
37063
+ ...typeof course?.section === "string" ? { section: course.section } : {}
37064
+ }
37065
+ };
37066
+ }
36198
37067
  function registerClassroomTools(server) {
36199
37068
  server.registerTool("gog_classroom_courses_list", {
36200
37069
  description: "List Google Classroom courses.",
@@ -36228,7 +37097,7 @@ function registerClassroomTools(server) {
36228
37097
  account: accountParam
36229
37098
  })
36230
37099
  }, async ({ courseId, account }) => {
36231
- return runOrDiagnose(["classroom", "courses", "get", courseId], { account });
37100
+ return runOrDiagnose(["classroom", "courses", "get", pos(courseId)], { account });
36232
37101
  });
36233
37102
  server.registerTool("gog_classroom_students_list", {
36234
37103
  description: "List students enrolled in a Google Classroom course.",
@@ -36242,7 +37111,7 @@ function registerClassroomTools(server) {
36242
37111
  account: accountParam
36243
37112
  })
36244
37113
  }, async ({ courseId, max, pageToken, page, all, account }) => {
36245
- const args = ["classroom", "students", "list", courseId];
37114
+ const args = ["classroom", "students", "list", pos(courseId)];
36246
37115
  if (max !== void 0) args.push(`--max=${max}`);
36247
37116
  const token = resolvePageToken({ pageToken, page });
36248
37117
  if (token) args.push(`--page=${token}`);
@@ -36258,7 +37127,7 @@ function registerClassroomTools(server) {
36258
37127
  account: accountParam
36259
37128
  })
36260
37129
  }, async ({ courseId, userId, account }) => {
36261
- return runOrDiagnose(["classroom", "students", "get", courseId, userId], { account });
37130
+ return runOrDiagnose(["classroom", "students", "get", pos(courseId), pos(userId)], { account });
36262
37131
  });
36263
37132
  server.registerTool("gog_classroom_teachers_list", {
36264
37133
  description: "List teachers in a Google Classroom course.",
@@ -36272,7 +37141,7 @@ function registerClassroomTools(server) {
36272
37141
  account: accountParam
36273
37142
  })
36274
37143
  }, async ({ courseId, max, pageToken, page, all, account }) => {
36275
- const args = ["classroom", "teachers", "list", courseId];
37144
+ const args = ["classroom", "teachers", "list", pos(courseId)];
36276
37145
  if (max !== void 0) args.push(`--max=${max}`);
36277
37146
  const token = resolvePageToken({ pageToken, page });
36278
37147
  if (token) args.push(`--page=${token}`);
@@ -36288,7 +37157,7 @@ function registerClassroomTools(server) {
36288
37157
  account: accountParam
36289
37158
  })
36290
37159
  }, async ({ courseId, userId, account }) => {
36291
- return runOrDiagnose(["classroom", "teachers", "get", courseId, userId], { account });
37160
+ return runOrDiagnose(["classroom", "teachers", "get", pos(courseId), pos(userId)], { account });
36292
37161
  });
36293
37162
  server.registerTool("gog_classroom_roster", {
36294
37163
  description: "List the full roster (students and/or teachers) of a Google Classroom course. Omit both flags to return both groups.",
@@ -36304,7 +37173,7 @@ function registerClassroomTools(server) {
36304
37173
  account: accountParam
36305
37174
  })
36306
37175
  }, async ({ courseId, students, teachers, max, pageToken, page, all, account }) => {
36307
- const args = ["classroom", "roster", courseId];
37176
+ const args = ["classroom", "roster", pos(courseId)];
36308
37177
  if (students) args.push("--students");
36309
37178
  if (teachers) args.push("--teachers");
36310
37179
  if (max !== void 0) args.push(`--max=${max}`);
@@ -36329,7 +37198,7 @@ function registerClassroomTools(server) {
36329
37198
  account: accountParam
36330
37199
  })
36331
37200
  }, async ({ courseId, state, topic, orderBy, max, pageToken, page, all, scanPages, account }) => {
36332
- const args = ["classroom", "coursework", "list", courseId];
37201
+ const args = ["classroom", "coursework", "list", pos(courseId)];
36333
37202
  if (state) args.push(`--state=${state}`);
36334
37203
  if (topic) args.push(`--topic=${topic}`);
36335
37204
  if (orderBy) args.push(`--order-by=${orderBy}`);
@@ -36349,7 +37218,7 @@ function registerClassroomTools(server) {
36349
37218
  account: accountParam
36350
37219
  })
36351
37220
  }, async ({ courseId, courseworkId, account }) => {
36352
- return runOrDiagnose(["classroom", "coursework", "get", courseId, courseworkId], { account });
37221
+ return runOrDiagnose(["classroom", "coursework", "get", pos(courseId), pos(courseworkId)], { account });
36353
37222
  });
36354
37223
  server.registerTool("gog_classroom_submissions_list", {
36355
37224
  description: "List student submissions for a coursework item.",
@@ -36367,7 +37236,7 @@ function registerClassroomTools(server) {
36367
37236
  account: accountParam
36368
37237
  })
36369
37238
  }, async ({ courseId, courseworkId, state, late, user, max, pageToken, page, all, account }) => {
36370
- const args = ["classroom", "submissions", "list", courseId, courseworkId];
37239
+ const args = ["classroom", "submissions", "list", pos(courseId), pos(courseworkId)];
36371
37240
  if (state) args.push(`--state=${state}`);
36372
37241
  if (late) args.push(`--late=${late}`);
36373
37242
  if (user) args.push(`--user=${user}`);
@@ -36387,7 +37256,7 @@ function registerClassroomTools(server) {
36387
37256
  account: accountParam
36388
37257
  })
36389
37258
  }, async ({ courseId, courseworkId, submissionId, account }) => {
36390
- return runOrDiagnose(["classroom", "submissions", "get", courseId, courseworkId, submissionId], { account });
37259
+ return runOrDiagnose(["classroom", "submissions", "get", pos(courseId), pos(courseworkId), pos(submissionId)], { account });
36391
37260
  });
36392
37261
  server.registerTool("gog_classroom_submissions_grade", {
36393
37262
  description: "Grade a student submission. Set draft and/or assigned grade values.",
@@ -36401,7 +37270,7 @@ function registerClassroomTools(server) {
36401
37270
  account: accountParam
36402
37271
  })
36403
37272
  }, async ({ courseId, courseworkId, submissionId, draft, assigned, account }) => {
36404
- const args = ["classroom", "submissions", "grade", courseId, courseworkId, submissionId];
37273
+ const args = ["classroom", "submissions", "grade", pos(courseId), pos(courseworkId), pos(submissionId)];
36405
37274
  if (draft) args.push(`--draft=${draft}`);
36406
37275
  if (assigned) args.push(`--assigned=${assigned}`);
36407
37276
  return runOrDiagnose(args, { account });
@@ -36416,7 +37285,7 @@ function registerClassroomTools(server) {
36416
37285
  account: accountParam
36417
37286
  })
36418
37287
  }, async ({ courseId, courseworkId, submissionId, account }) => {
36419
- return runOrDiagnose(["classroom", "submissions", "return", courseId, courseworkId, submissionId], { account });
37288
+ return runOrDiagnose(["classroom", "submissions", "return", pos(courseId), pos(courseworkId), pos(submissionId)], { account });
36420
37289
  });
36421
37290
  server.registerTool("gog_classroom_submissions_turn_in", {
36422
37291
  description: "Turn in a student submission (student action).",
@@ -36428,7 +37297,7 @@ function registerClassroomTools(server) {
36428
37297
  account: accountParam
36429
37298
  })
36430
37299
  }, async ({ courseId, courseworkId, submissionId, account }) => {
36431
- return runOrDiagnose(["classroom", "submissions", "turn-in", courseId, courseworkId, submissionId], { account });
37300
+ return runOrDiagnose(["classroom", "submissions", "turn-in", pos(courseId), pos(courseworkId), pos(submissionId)], { account });
36432
37301
  });
36433
37302
  server.registerTool("gog_classroom_submissions_reclaim", {
36434
37303
  description: "Reclaim a turned-in submission (student action to edit a submission).",
@@ -36440,7 +37309,7 @@ function registerClassroomTools(server) {
36440
37309
  account: accountParam
36441
37310
  })
36442
37311
  }, async ({ courseId, courseworkId, submissionId, account }) => {
36443
- return runOrDiagnose(["classroom", "submissions", "reclaim", courseId, courseworkId, submissionId], { account });
37312
+ return runOrDiagnose(["classroom", "submissions", "reclaim", pos(courseId), pos(courseworkId), pos(submissionId)], { account });
36444
37313
  });
36445
37314
  server.registerTool("gog_classroom_announcements_list", {
36446
37315
  description: "List announcements in a Google Classroom course.",
@@ -36456,7 +37325,7 @@ function registerClassroomTools(server) {
36456
37325
  account: accountParam
36457
37326
  })
36458
37327
  }, async ({ courseId, state, orderBy, max, pageToken, page, all, account }) => {
36459
- const args = ["classroom", "announcements", "list", courseId];
37328
+ const args = ["classroom", "announcements", "list", pos(courseId)];
36460
37329
  if (state) args.push(`--state=${state}`);
36461
37330
  if (orderBy) args.push(`--order-by=${orderBy}`);
36462
37331
  if (max !== void 0) args.push(`--max=${max}`);
@@ -36474,22 +37343,45 @@ function registerClassroomTools(server) {
36474
37343
  account: accountParam
36475
37344
  })
36476
37345
  }, async ({ courseId, announcementId, account }) => {
36477
- return runOrDiagnose(["classroom", "announcements", "get", courseId, announcementId], { account });
37346
+ return runOrDiagnose(["classroom", "announcements", "get", pos(courseId), pos(announcementId)], { account });
36478
37347
  });
36479
37348
  server.registerTool("gog_classroom_announcements_create", {
36480
- description: "Create an announcement in a Google Classroom course.",
37349
+ 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,
36481
37350
  annotations: { destructiveHint: true },
36482
37351
  inputSchema: external_exports.object({
36483
37352
  courseId: external_exports.string().describe("Course ID"),
36484
37353
  text: external_exports.string().describe("Announcement text"),
36485
- state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State"),
37354
+ state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State (DRAFT is visible only to teachers and needs no confirmation)"),
36486
37355
  scheduled: external_exports.string().optional().describe("Scheduled publish time"),
36487
- account: accountParam
37356
+ account: accountParam,
37357
+ confirmToken: confirmTokenParam
36488
37358
  })
36489
- }, async ({ courseId, text, state, scheduled, account }) => {
36490
- const args = ["classroom", "announcements", "create", courseId, `--text=${text}`];
37359
+ }, async ({ courseId, text, state, scheduled, account, confirmToken }, ctx) => {
37360
+ const args = ["classroom", "announcements", "create", pos(courseId), `--text=${text}`];
36491
37361
  if (state) args.push(`--state=${state}`);
36492
37362
  if (scheduled) args.push(`--scheduled=${scheduled}`);
37363
+ if (state !== "DRAFT") {
37364
+ const read = await readCourse(courseId, account);
37365
+ if (read.error) return read.error;
37366
+ const publishes = scheduled ? `at ${scheduled}` : "immediately";
37367
+ const confirmation = await requireDispatchConfirmation(ctx, {
37368
+ action: "classroom.announcement-create",
37369
+ message: "Review and confirm this Classroom announcement:",
37370
+ confirmationLabel: "Confirm that this announcement should be posted to the class.",
37371
+ details: { course: read.course, publishes, textPreview: bodyPreview(text) },
37372
+ unsupportedNote: "Create it with state DRAFT instead; the user can review and post it from Classroom.",
37373
+ fallback: {
37374
+ tool: "gog_classroom_announcements_create",
37375
+ account,
37376
+ confirmToken,
37377
+ subject: () => {
37378
+ const view = { course: read.course, publishes, text };
37379
+ return { target: courseId, payload: view, preview: view };
37380
+ }
37381
+ }
37382
+ });
37383
+ if (confirmation) return confirmation;
37384
+ }
36493
37385
  return runOrDiagnose(args, { account });
36494
37386
  });
36495
37387
  server.registerTool("gog_classroom_topics_list", {
@@ -36504,7 +37396,7 @@ function registerClassroomTools(server) {
36504
37396
  account: accountParam
36505
37397
  })
36506
37398
  }, async ({ courseId, max, pageToken, page, all, account }) => {
36507
- const args = ["classroom", "topics", "list", courseId];
37399
+ const args = ["classroom", "topics", "list", pos(courseId)];
36508
37400
  if (max !== void 0) args.push(`--max=${max}`);
36509
37401
  const token = resolvePageToken({ pageToken, page });
36510
37402
  if (token) args.push(`--page=${token}`);
@@ -36520,7 +37412,7 @@ function registerClassroomTools(server) {
36520
37412
  account: accountParam
36521
37413
  })
36522
37414
  }, async ({ courseId, topicId, account }) => {
36523
- return runOrDiagnose(["classroom", "topics", "get", courseId, topicId], { account });
37415
+ return runOrDiagnose(["classroom", "topics", "get", pos(courseId), pos(topicId)], { account });
36524
37416
  });
36525
37417
  server.registerTool("gog_classroom_invitations_list", {
36526
37418
  description: "List Google Classroom invitations.",
@@ -36552,7 +37444,7 @@ function registerClassroomTools(server) {
36552
37444
  account: accountParam
36553
37445
  })
36554
37446
  }, async ({ invitationId, account }) => {
36555
- return runOrDiagnose(["classroom", "invitations", "get", invitationId], { account });
37447
+ return runOrDiagnose(["classroom", "invitations", "get", pos(invitationId)], { account });
36556
37448
  });
36557
37449
  server.registerTool("gog_classroom_invitations_accept", {
36558
37450
  description: "Accept a Google Classroom invitation.",
@@ -36562,7 +37454,7 @@ function registerClassroomTools(server) {
36562
37454
  account: accountParam
36563
37455
  })
36564
37456
  }, async ({ invitationId, account }) => {
36565
- return runOrDiagnose(["classroom", "invitations", "accept", invitationId], { account });
37457
+ return runOrDiagnose(["classroom", "invitations", "accept", pos(invitationId)], { account });
36566
37458
  });
36567
37459
  server.registerTool("gog_classroom_profile_get", {
36568
37460
  description: "Get a Google Classroom user profile. Omit userId to fetch the authenticated user.",
@@ -36573,12 +37465,13 @@ function registerClassroomTools(server) {
36573
37465
  })
36574
37466
  }, async ({ userId, account }) => {
36575
37467
  const args = ["classroom", "profile", "get"];
36576
- if (userId) args.push(userId);
37468
+ if (userId) args.push(pos(userId));
36577
37469
  return runOrDiagnose(args, { account });
36578
37470
  });
36579
37471
  registerRunTool(server, {
36580
37472
  service: "classroom",
36581
37473
  examples: '"guardians", "materials", "guardian-invitations"',
37474
+ vet: vetClassroomRun,
36582
37475
  note: "Covers anything not wrapped by the dedicated tools (guardians, guardian-invitations, materials, coursework assignees, announcement assignees, etc.)."
36583
37476
  });
36584
37477
  }
@@ -36593,7 +37486,7 @@ function registerContactsTools(server) {
36593
37486
  account: accountParam
36594
37487
  })
36595
37488
  }, async ({ query, account }) => {
36596
- return runOrDiagnose(["contacts", "search", query], { account });
37489
+ return runOrDiagnose(["contacts", "search", pos(query)], { account });
36597
37490
  });
36598
37491
  server.registerTool("gog_contacts_list", {
36599
37492
  description: "List all Google Contacts.",
@@ -36612,7 +37505,7 @@ function registerContactsTools(server) {
36612
37505
  account: accountParam
36613
37506
  })
36614
37507
  }, async ({ resourceName, account }) => {
36615
- return runOrDiagnose(["contacts", "get", resourceName], { account });
37508
+ return runOrDiagnose(["contacts", "get", pos(resourceName)], { account });
36616
37509
  });
36617
37510
  server.registerTool("gog_contacts_create", {
36618
37511
  description: "Create a new Google Contact.",
@@ -36648,7 +37541,7 @@ function registerDocsTools(server) {
36648
37541
  account: accountParam
36649
37542
  })
36650
37543
  }, async ({ docId, account }) => {
36651
- return runOrDiagnose(["docs", "info", docId], { account });
37544
+ return runOrDiagnose(["docs", "info", pos(docId)], { account });
36652
37545
  });
36653
37546
  server.registerTool("gog_docs_cat", {
36654
37547
  description: "Read a Google Doc as plain text.",
@@ -36659,7 +37552,7 @@ function registerDocsTools(server) {
36659
37552
  account: accountParam
36660
37553
  })
36661
37554
  }, async ({ docId, chips, account }) => {
36662
- const args = ["docs", "cat", docId];
37555
+ const args = ["docs", "cat", pos(docId)];
36663
37556
  if (chips) args.push("--chips");
36664
37557
  return runOrDiagnose(args, { account });
36665
37558
  });
@@ -36671,7 +37564,7 @@ function registerDocsTools(server) {
36671
37564
  account: accountParam
36672
37565
  })
36673
37566
  }, async ({ title, account }) => {
36674
- return runOrDiagnose(["docs", "create", title], { account });
37567
+ return runOrDiagnose(["docs", "create", pos(title)], { account });
36675
37568
  });
36676
37569
  server.registerTool("gog_docs_write", {
36677
37570
  description: "Write text content to a Google Doc, replacing existing body content by default. Set append=true to add after existing content.",
@@ -36696,7 +37589,7 @@ function registerDocsTools(server) {
36696
37589
  account: accountParam
36697
37590
  })
36698
37591
  }, async ({ docId, text, append, checkOrphans, bullets, bulletPreset, ordered, noBullets, indentStart, indentEnd, indentFirstLine, spaceAbove, spaceBelow, keepLinesTogether, keepWithNext, batch, account }) => {
36699
- const args = ["docs", "write", docId, `--text=${text}`];
37592
+ const args = ["docs", "write", pos(docId), `--text=${text}`];
36700
37593
  if (append) args.push("--append");
36701
37594
  if (checkOrphans) args.push("--check-orphans");
36702
37595
  if (bullets) args.push("--bullets");
@@ -36723,7 +37616,7 @@ function registerDocsTools(server) {
36723
37616
  account: accountParam
36724
37617
  })
36725
37618
  }, async ({ docId, find, replace, account }) => {
36726
- return runOrDiagnose(["docs", "find-replace", docId, find, replace], { account });
37619
+ return runOrDiagnose(["docs", "find-replace", pos(docId), pos(find), pos(replace)], { account });
36727
37620
  });
36728
37621
  server.registerTool("gog_docs_structure", {
36729
37622
  description: "Show a Google Doc's structure with numbered paragraphs. Useful for understanding the document layout before making index-based edits.",
@@ -36733,18 +37626,31 @@ function registerDocsTools(server) {
36733
37626
  account: accountParam
36734
37627
  })
36735
37628
  }, async ({ docId, account }) => {
36736
- return runOrDiagnose(["docs", "structure", docId], { account });
37629
+ return runOrDiagnose(["docs", "structure", pos(docId)], { account });
36737
37630
  });
36738
37631
  registerRunTool(server, { service: "docs", examples: '"copy", "clear", "insert", "sed", "export"' });
36739
37632
  }
36740
37633
 
36741
37634
  // src/tools/drive.ts
37635
+ function vetDriveRun(subcommand, _args) {
37636
+ return subcommand.toLowerCase() === "share" ? gatedElsewhere("gog drive share", "gog_drive_run", "grants access to a file", "gog_drive_share") : void 0;
37637
+ }
36742
37638
  var GOOGLE_DOC_MIME = "application/vnd.google-apps.document";
37639
+ function shareTargetMeta(raw) {
37640
+ try {
37641
+ const { name, mimeType } = fileMeta(raw);
37642
+ return { name, mimeType };
37643
+ } catch {
37644
+ return {};
37645
+ }
37646
+ }
36743
37647
  function fileMeta(raw) {
36744
37648
  const parsed = JSON.parse(raw);
36745
37649
  const f = parsed.file ?? parsed;
36746
- return { name: f.name, mimeType: f.mimeType };
37650
+ const size = f.size === void 0 ? void 0 : Number(f.size);
37651
+ return { name: f.name, mimeType: f.mimeType, size: Number.isFinite(size) ? size : void 0 };
36747
37652
  }
37653
+ var MAX_DRIVE_READ_BYTES = MAX_INLINE_ATTACHMENT_BYTES;
36748
37654
  var DRIVE_LS_COMPACT_FIELDS = "nextPageToken,files(id,name,mimeType,modifiedTime,size,webViewLink)";
36749
37655
  function registerDriveTools(server) {
36750
37656
  server.registerTool("gog_drive_ls", {
@@ -36788,7 +37694,7 @@ function registerDriveTools(server) {
36788
37694
  account: accountParam
36789
37695
  })
36790
37696
  }, async ({ query, view, account }) => {
36791
- return runOrDiagnose(["drive", "search", query], {
37697
+ return runOrDiagnose(["drive", "search", pos(query)], {
36792
37698
  account,
36793
37699
  stripMedia: resolveView(view, ["compact", "full"]) === "compact"
36794
37700
  });
@@ -36808,7 +37714,7 @@ function registerDriveTools(server) {
36808
37714
  account: accountParam
36809
37715
  })
36810
37716
  }, async ({ fileId, view, account }) => {
36811
- return runOrDiagnose(["drive", "get", fileId], {
37717
+ return runOrDiagnose(["drive", "get", pos(fileId)], {
36812
37718
  account,
36813
37719
  stripMedia: resolveView(view, ["compact", "full"]) === "compact"
36814
37720
  });
@@ -36821,7 +37727,7 @@ function registerDriveTools(server) {
36821
37727
  account: accountParam
36822
37728
  })
36823
37729
  }, async ({ name, account }) => {
36824
- return runOrDiagnose(["drive", "mkdir", name], { account });
37730
+ return runOrDiagnose(["drive", "mkdir", pos(name)], { account });
36825
37731
  });
36826
37732
  server.registerTool("gog_drive_rename", {
36827
37733
  description: "Rename a file or folder in Google Drive.",
@@ -36832,7 +37738,7 @@ function registerDriveTools(server) {
36832
37738
  account: accountParam
36833
37739
  })
36834
37740
  }, async ({ fileId, newName, account }) => {
36835
- return runOrDiagnose(["drive", "rename", fileId, newName], { account });
37741
+ return runOrDiagnose(["drive", "rename", pos(fileId), pos(newName)], { account });
36836
37742
  });
36837
37743
  server.registerTool("gog_drive_move", {
36838
37744
  description: "Move a file to a different folder in Google Drive.",
@@ -36843,7 +37749,7 @@ function registerDriveTools(server) {
36843
37749
  account: accountParam
36844
37750
  })
36845
37751
  }, async ({ fileId, parentId, account }) => {
36846
- return runOrDiagnose(["drive", "move", fileId, `--parent=${parentId}`], { account });
37752
+ return runOrDiagnose(["drive", "move", pos(fileId), `--parent=${parentId}`], { account });
36847
37753
  });
36848
37754
  server.registerTool("gog_drive_delete", {
36849
37755
  description: "Move a Google Drive file to trash, or permanently delete it with permanent=true (irreversible).",
@@ -36854,13 +37760,13 @@ function registerDriveTools(server) {
36854
37760
  account: accountParam
36855
37761
  })
36856
37762
  }, async ({ fileId, permanent, account }) => {
36857
- const args = ["drive", "delete", fileId];
37763
+ const args = ["drive", "delete", pos(fileId)];
36858
37764
  if (permanent) args.push("--permanent");
36859
37765
  args.push("--force");
36860
37766
  return runOrDiagnose(args, { account });
36861
37767
  });
36862
37768
  server.registerTool("gog_drive_share", {
36863
- description: "Share a Google Drive file or folder.",
37769
+ 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,
36864
37770
  annotations: { destructiveHint: true },
36865
37771
  inputSchema: external_exports.object({
36866
37772
  fileId: external_exports.string().describe("File or folder ID"),
@@ -36868,10 +37774,36 @@ function registerDriveTools(server) {
36868
37774
  email: external_exports.string().optional().describe("User email (required when to=user)"),
36869
37775
  domain: external_exports.string().optional().describe("Domain (required when to=domain)"),
36870
37776
  role: external_exports.enum(["reader", "writer"]).optional().describe("Permission role (default: reader)"),
36871
- account: accountParam
37777
+ account: accountParam,
37778
+ confirmToken: confirmTokenParam
36872
37779
  })
36873
- }, async ({ fileId, to, email: email3, domain: domain2, role, account }) => {
36874
- const args = ["drive", "share", fileId, `--to=${to}`];
37780
+ }, async ({ fileId, to, email: email3, domain: domain2, role, account, confirmToken }, ctx) => {
37781
+ const got = await runOrDiagnose(["drive", "get", pos(fileId)], { account });
37782
+ if (got.isError) return got;
37783
+ const file2 = { id: fileId, ...shareTargetMeta(resultText(got)) };
37784
+ const grant = {
37785
+ file: file2,
37786
+ to,
37787
+ ...email3 ? { email: email3 } : {},
37788
+ ...domain2 ? { domain: domain2 } : {},
37789
+ role: role ?? "reader",
37790
+ publicLink: to === "anyone"
37791
+ };
37792
+ const confirmation = await requireDispatchConfirmation(ctx, {
37793
+ action: "drive.share",
37794
+ message: to === "anyone" ? "Review and confirm making this file available to ANYONE with the link:" : "Review and confirm this Drive share:",
37795
+ confirmationLabel: "Confirm that this access should be granted now.",
37796
+ details: grant,
37797
+ unsupportedNote: "Ask the user to share it themselves from Google Drive.",
37798
+ fallback: {
37799
+ tool: "gog_drive_share",
37800
+ account,
37801
+ confirmToken,
37802
+ subject: () => ({ target: fileId, payload: grant, preview: grant })
37803
+ }
37804
+ });
37805
+ if (confirmation) return confirmation;
37806
+ const args = ["drive", "share", pos(fileId), `--to=${to}`];
36875
37807
  if (email3) args.push(`--email=${email3}`);
36876
37808
  if (domain2) args.push(`--domain=${domain2}`);
36877
37809
  if (role) args.push(`--role=${role}`);
@@ -36894,7 +37826,7 @@ function registerDriveTools(server) {
36894
37826
  }, async ({ fileId, ocrLanguage, offset = 0, maxChars, account }) => {
36895
37827
  let tempDocId;
36896
37828
  try {
36897
- const { name, mimeType } = fileMeta(await run(["drive", "get", fileId], { account }));
37829
+ const { name, mimeType } = fileMeta(await run(["drive", "get", pos(fileId)], { account }));
36898
37830
  let sourceId = fileId;
36899
37831
  if (mimeType !== GOOGLE_DOC_MIME) {
36900
37832
  const params = JSON.stringify({ fileId, ...ocrLanguage ? { ocrLanguage } : {} });
@@ -36939,13 +37871,13 @@ function registerDriveTools(server) {
36939
37871
  return diagnose(err);
36940
37872
  } finally {
36941
37873
  if (tempDocId) {
36942
- await run(["drive", "delete", tempDocId, "--permanent", "--force"], { account }).catch(() => {
37874
+ await run(["drive", "delete", pos(tempDocId), "--permanent", "--force"], { account }).catch(() => {
36943
37875
  });
36944
37876
  }
36945
37877
  }
36946
37878
  });
36947
37879
  server.registerTool("gog_drive_read_bytes", {
36948
- 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.",
37880
+ 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.",
36949
37881
  annotations: { readOnlyHint: true },
36950
37882
  inputSchema: external_exports.object({
36951
37883
  fileId: external_exports.string().describe("Drive file ID"),
@@ -36953,9 +37885,17 @@ function registerDriveTools(server) {
36953
37885
  })
36954
37886
  }, async ({ fileId, account }) => {
36955
37887
  try {
36956
- const { name, mimeType } = fileMeta(await run(["drive", "get", fileId], { account }));
37888
+ const { name, mimeType, size } = fileMeta(await run(["drive", "get", pos(fileId)], { account }));
37889
+ if (size !== void 0 && size > MAX_DRIVE_READ_BYTES) {
37890
+ return errorResult(
37891
+ `${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.`
37892
+ );
37893
+ }
36957
37894
  const params = JSON.stringify({ fileId, alt: "media" });
36958
- const blob = await runBinary(["api", "call", "drive", "v3", "files.get", `--params=${params}`], { account });
37895
+ const blob = await runBinary(
37896
+ ["api", "call", "drive", "v3", "files.get", `--params=${params}`],
37897
+ { account, maxOutputBytes: MAX_DRIVE_READ_BYTES }
37898
+ );
36959
37899
  const type = mimeType ?? "application/octet-stream";
36960
37900
  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).`;
36961
37901
  return {
@@ -36975,7 +37915,7 @@ function registerDriveTools(server) {
36975
37915
  return diagnose(err);
36976
37916
  }
36977
37917
  });
36978
- registerRunTool(server, { service: "drive", examples: '"copy", "upload", "download", "permissions"' });
37918
+ registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"', vet: vetDriveRun });
36979
37919
  }
36980
37920
 
36981
37921
  // src/gmail-results.ts
@@ -37003,7 +37943,7 @@ async function countMatches(method, itemsKey, query, account) {
37003
37943
  maxResults: COUNT_PROBE_PAGE_SIZE,
37004
37944
  fields: `${itemsKey}/id,nextPageToken`
37005
37945
  });
37006
- const raw = await run(["api", "call", "gmail", "v1", method, `--params=${params}`], { account });
37946
+ const raw = await run(["api", "call", "gmail", "v1", pos(method), `--params=${params}`], { account });
37007
37947
  const parsed = JSON.parse(raw);
37008
37948
  const items = parsed[itemsKey];
37009
37949
  if (!Array.isArray(items)) return {};
@@ -37042,7 +37982,7 @@ async function fetchGmailPages(runPage, itemsKey, maxPages, startToken) {
37042
37982
  const result = await runPage(token);
37043
37983
  const parsed = parsePage(result, itemsKey);
37044
37984
  if (parsed === void 0) {
37045
- return base === void 0 ? result : finish(base, itemsKey, merged, token);
37985
+ return base === void 0 ? result : finish(base, itemsKey, merged, token, pageErrorText(result, itemsKey, pages + 1));
37046
37986
  }
37047
37987
  base = parsed;
37048
37988
  merged.push(...parsed[itemsKey]);
@@ -37073,10 +38013,18 @@ function parsePage(result, itemsKey) {
37073
38013
  const obj = parsed;
37074
38014
  return Array.isArray(obj[itemsKey]) ? obj : void 0;
37075
38015
  }
37076
- function finish(base, itemsKey, merged, token) {
38016
+ var PAGE_ERROR_MAX = 2e3;
38017
+ function pageErrorText(result, itemsKey, n) {
38018
+ const first = result.content[0];
38019
+ const text = first?.type === "text" ? first.text.slice(0, PAGE_ERROR_MAX) : "";
38020
+ if (result.isError) return `page ${n} failed${text ? `: ${text}` : ""}`;
38021
+ return `page ${n} returned output that is not a ${itemsKey} list`;
38022
+ }
38023
+ function finish(base, itemsKey, merged, token, pageError) {
37077
38024
  const out = { ...base, [itemsKey]: merged };
37078
38025
  if (token === void 0) delete out.nextPageToken;
37079
38026
  else out.nextPageToken = token;
38027
+ if (pageError !== void 0) out.pageError = pageError;
37080
38028
  return rawTextResult(JSON.stringify(out));
37081
38029
  }
37082
38030
 
@@ -37090,16 +38038,19 @@ var STAGING_TWIN = {
37090
38038
  "gmail.reply-all": "gog_gmail_drafts_reply_all",
37091
38039
  "gmail.send": "gog_gmail_drafts_create"
37092
38040
  };
37093
- function requireGmailDispatchConfirmation(ctx, op, details) {
38041
+ var UNSUPPORTED_NOTE2 = {
38042
+ "gmail.drafts-send": "The draft is still saved: ask the user to review it and send it from Gmail."
38043
+ };
38044
+ async function requireGmailDispatchConfirmation(ctx, op, details, fallback) {
37094
38045
  const twin = STAGING_TWIN[op];
37095
- return requireConfirmation(ctx, {
38046
+ 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];
38047
+ return requireDispatchConfirmation(ctx, {
37096
38048
  action: op,
37097
- message: "Review and confirm this email dispatch:",
38049
+ message: op === "gmail.filter-forward" ? "Review and confirm this mail-forwarding filter:" : "Review and confirm this email dispatch:",
37098
38050
  details,
37099
- confirmationLabel: "Confirm that this email should be sent now.",
37100
- ...twin ? {
37101
- unsupportedNote: `Stage it with ${twin} instead, review the draft, and send it with gog_gmail_drafts_send.`
37102
- } : {}
38051
+ confirmationLabel: op === "gmail.filter-forward" ? "Confirm that matching mail should be forwarded automatically from now on." : "Confirm that this email should be sent now.",
38052
+ unsupportedNote: note,
38053
+ ...fallback ? { fallback: { instruction: CONFIRM_SEND_INSTRUCTION, ...fallback } } : {}
37103
38054
  });
37104
38055
  }
37105
38056
  var EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/gi;
@@ -37153,10 +38104,6 @@ function logGmailDispatch(tool, recipients, account) {
37153
38104
  process.stderr.write(`${JSON.stringify(event)}
37154
38105
  `);
37155
38106
  }
37156
- function resultText(result) {
37157
- const first = result.content[0];
37158
- return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
37159
- }
37160
38107
 
37161
38108
  // src/tools/gmail.ts
37162
38109
  var replySchema = {
@@ -37170,7 +38117,7 @@ var replySchema = {
37170
38117
  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."),
37171
38118
  subject: external_exports.string().optional().describe('Override reply subject (default: "Re: <original>"). A changed subject starts a NEW Gmail thread.'),
37172
38119
  noQuote: external_exports.boolean().optional().describe("Do not include the original message quoted below the reply (default: the original is quoted)"),
37173
- 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.`),
38120
+ 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).`),
37174
38121
  attachInline: attachInlineParam,
37175
38122
  from: external_exports.string().optional().describe("Send from this email address (must be a verified send-as alias)"),
37176
38123
  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."),
@@ -37179,8 +38126,14 @@ var replySchema = {
37179
38126
  signatureFile: external_exports.string().optional().describe("Append a local signature file (plain text or HTML), read on the gog server"),
37180
38127
  account: accountParam
37181
38128
  };
38129
+ function confineReplyPaths(f) {
38130
+ confinePaths(f.attach, "attach");
38131
+ if (f.bodyHtmlFile) confinePath(f.bodyHtmlFile, "bodyHtmlFile");
38132
+ if (f.signatureFile) confinePath(f.signatureFile, "signatureFile");
38133
+ }
37182
38134
  function appendReplyFlags(args, f) {
37183
38135
  assertNotBoth("bodyHtml", "bodyHtmlFile", f.bodyHtml, f.bodyHtmlFile);
38136
+ confineReplyPaths(f);
37184
38137
  if (f.body) args.push(payloadArg("body", "body-file", f.body));
37185
38138
  if (f.bodyHtml) args.push(payloadArg("body-html", "body-html-file", f.bodyHtml, "html"));
37186
38139
  else if (f.bodyHtmlFile) args.push(`--body-html-file=${f.bodyHtmlFile}`);
@@ -37198,7 +38151,7 @@ function appendReplyFlags(args, f) {
37198
38151
  if (f.signatureFile) args.push(`--signature-file=${f.signatureFile}`);
37199
38152
  args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
37200
38153
  }
37201
- var sendReplySchema = external_exports.object(replySchema);
38154
+ var sendReplySchema = external_exports.object({ ...replySchema, confirmToken: confirmTokenParam });
37202
38155
  function parseMetadataHeaders(raw) {
37203
38156
  let parsed;
37204
38157
  try {
@@ -37223,27 +38176,110 @@ function computeReplyRecipients(kind, headers, flags) {
37223
38176
  }
37224
38177
  return emails;
37225
38178
  }
37226
- async function sendReply(kind, toolName, messageId, account, flags, ctx) {
37227
- const metaResult = await runOrDiagnose(["gmail", "get", messageId, "--format=metadata"], { account });
38179
+ function metadataThreadId(raw) {
38180
+ try {
38181
+ const threadId = JSON.parse(raw)?.message?.threadId;
38182
+ return typeof threadId === "string" ? threadId : void 0;
38183
+ } catch {
38184
+ return void 0;
38185
+ }
38186
+ }
38187
+ function replyTokenSubject(kind, messageId, account, headers, threadId, flags) {
38188
+ const removed = new Set(extractEmails(...flags.remove ?? []));
38189
+ const keep = (emails) => emails.filter((e) => !removed.has(e));
38190
+ const to = keep(extractEmails(headers.from, ...kind === "reply-all" ? [headers.to] : [], ...flags.to ?? []));
38191
+ const cc = keep(extractEmails(...kind === "reply-all" ? [headers.cc] : [], ...flags.cc ?? []));
38192
+ const bcc = keep(extractEmails(...flags.bcc ?? []));
38193
+ const attachments = attachmentDetails(flags.attach, flags.attachInline);
38194
+ const subject = flags.subject || (headers.subject ? `Re: ${headers.subject}` : void 0);
38195
+ const from = flags.from ?? (flags.autoFromAddressedAlias ? "the send-as alias the original was addressed to" : senderPreview(account));
38196
+ const localFiles = attachmentDetails([flags.bodyHtmlFile, flags.signatureFile].filter((p) => Boolean(p)), void 0);
38197
+ return {
38198
+ target: messageId,
38199
+ payload: {
38200
+ kind,
38201
+ to,
38202
+ cc,
38203
+ bcc,
38204
+ subject,
38205
+ from,
38206
+ body: flags.body,
38207
+ bodyHtml: flags.bodyHtml,
38208
+ localFiles,
38209
+ signature: Boolean(flags.signature),
38210
+ signatureFrom: flags.signatureFrom,
38211
+ attachments,
38212
+ quote: !flags.noQuote,
38213
+ threadId,
38214
+ inReplyTo: headers.message_id,
38215
+ references: headers.references
38216
+ },
38217
+ preview: {
38218
+ from,
38219
+ to,
38220
+ cc,
38221
+ bcc,
38222
+ subject,
38223
+ body: flags.body,
38224
+ ...flags.bodyHtml ? { bodyHtml: flags.bodyHtml } : {},
38225
+ ...flags.bodyHtmlFile ? { bodyHtmlFile: flags.bodyHtmlFile } : {},
38226
+ ...flags.signature || flags.signatureFrom || flags.signatureFile ? { signature: flags.signatureFile ?? flags.signatureFrom ?? "the Gmail signature of the sending address" } : {},
38227
+ quotesOriginal: !flags.noQuote,
38228
+ attachments: attachmentPreview(attachments),
38229
+ replyingToMessageId: messageId,
38230
+ threadId,
38231
+ inReplyTo: headers.message_id
38232
+ }
38233
+ };
38234
+ }
38235
+ async function sendReply(kind, toolName, messageId, account, flags, ctx, confirmToken) {
38236
+ const metaResult = await runOrDiagnose(["gmail", "get", pos(messageId), "--format=metadata"], { account });
37228
38237
  if (metaResult.isError) return metaResult;
37229
- const headers = parseMetadataHeaders(resultText(metaResult));
38238
+ const metaText = resultText(metaResult);
38239
+ const headers = parseMetadataHeaders(metaText);
37230
38240
  const recipients = computeReplyRecipients(kind, headers, flags);
37231
- const confirmation = requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
38241
+ const confirmation = await requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
37232
38242
  messageId,
37233
38243
  recipients,
37234
38244
  recipientCount: recipients.length,
37235
38245
  subject: flags.subject || (headers.subject ? `Re: ${headers.subject}` : void 0),
37236
38246
  quoting: !flags.noQuote,
37237
38247
  bodyLength: (flags.body ?? flags.bodyHtml ?? "").length,
37238
- attachmentCount: (flags.attach?.length ?? 0) + (flags.attachInline?.length ?? 0)
38248
+ bodyPreview: bodyPreview(flags.body ?? flags.bodyHtml),
38249
+ bodyHtmlFile: flags.bodyHtmlFile,
38250
+ attachmentCount: (flags.attach?.length ?? 0) + (flags.attachInline?.length ?? 0),
38251
+ attachments: attachmentNames(flags.attach, flags.attachInline)
38252
+ }, {
38253
+ tool: toolName,
38254
+ account,
38255
+ confirmToken,
38256
+ // The metadata read above IS the phase-2 re-read: it runs on every call,
38257
+ // so the original's Message-ID/References are fresh here.
38258
+ subject: () => replyTokenSubject(kind, messageId, account, headers, metadataThreadId(metaText), flags)
37239
38259
  });
37240
38260
  if (confirmation) return confirmation;
37241
- const args = ["gmail", kind, messageId];
38261
+ const args = ["gmail", kind, pos(messageId)];
37242
38262
  appendReplyFlags(args, flags);
37243
38263
  const result = await runOrDiagnose(args, { account });
37244
38264
  if (!result.isError) logGmailDispatch(toolName, recipients, account);
37245
38265
  return result;
37246
38266
  }
38267
+ var GMAIL_RUN_BLOCKED_SETTINGS = /* @__PURE__ */ new Set(["forwarding", "autoforward", "filters", "delegates"]);
38268
+ function vetGmailRun(subcommand, args) {
38269
+ if (subcommand === "autoreply") {
38270
+ return "gog gmail autoreply sends mail and is not available through gog_gmail_run. Use gog_gmail_autoreply, which asks the user to confirm.";
38271
+ }
38272
+ if (GMAIL_RUN_BLOCKED_SETTINGS.has(subcommand)) {
38273
+ 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.`;
38274
+ }
38275
+ if (subcommand === "settings") {
38276
+ const blocked = hasCommandWord(args, GMAIL_RUN_BLOCKED_SETTINGS);
38277
+ if (blocked) {
38278
+ 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.`;
38279
+ }
38280
+ }
38281
+ return void 0;
38282
+ }
37247
38283
  function registerGmailTools(server) {
37248
38284
  server.registerTool("gog_gmail_search", {
37249
38285
  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.',
@@ -37259,7 +38295,7 @@ function registerGmailTools(server) {
37259
38295
  account: accountParam
37260
38296
  })
37261
38297
  }, async ({ query, max, pageToken, page, maxPages, all, fromContact, account }) => {
37262
- const args = ["gmail", "search", query];
38298
+ const args = ["gmail", "search", pos(query)];
37263
38299
  if (max !== void 0) args.push(`--max=${max}`);
37264
38300
  if (all) args.push("--all");
37265
38301
  if (fromContact) args.push(`--from-contact=${fromContact}`);
@@ -37290,13 +38326,13 @@ function registerGmailTools(server) {
37290
38326
  account: accountParam
37291
38327
  })
37292
38328
  }, async ({ messageId, format, sanitizeContent, account }) => {
37293
- const args = ["gmail", "get", messageId];
38329
+ const args = ["gmail", "get", pos(messageId)];
37294
38330
  if (format) args.push(`--format=${format}`);
37295
38331
  if (sanitizeContent) args.push("--sanitize-content");
37296
38332
  return runOrDiagnose(args, { account });
37297
38333
  });
37298
38334
  server.registerTool("gog_gmail_send", {
37299
- 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.',
38335
+ 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,
37300
38336
  annotations: { destructiveHint: true },
37301
38337
  inputSchema: external_exports.object({
37302
38338
  to: external_exports.string().describe("Recipient(s), comma-separated"),
@@ -37307,11 +38343,13 @@ function registerGmailTools(server) {
37307
38343
  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.'),
37308
38344
  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."),
37309
38345
  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."),
37310
- 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.`),
38346
+ 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).`),
37311
38347
  attachInline: attachInlineParam,
37312
- account: accountParam
38348
+ account: accountParam,
38349
+ confirmToken: confirmTokenParam
37313
38350
  })
37314
- }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account }, ctx) => {
38351
+ }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account, confirmToken }, ctx) => {
38352
+ confinePaths(attach, "attach");
37315
38353
  const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
37316
38354
  if (cc) args.push(`--cc=${cc}`);
37317
38355
  if (bcc) args.push(`--bcc=${bcc}`);
@@ -37322,7 +38360,7 @@ function registerGmailTools(server) {
37322
38360
  const inline = inlineAttachmentArgs("attach", attachInline, args);
37323
38361
  args.push(...inline);
37324
38362
  const recipients = extractEmails(to, cc, bcc);
37325
- const confirmation = requireGmailDispatchConfirmation(ctx, "gmail.send", {
38363
+ const confirmation = await requireGmailDispatchConfirmation(ctx, "gmail.send", {
37326
38364
  to,
37327
38365
  cc,
37328
38366
  bcc,
@@ -37330,9 +38368,37 @@ function registerGmailTools(server) {
37330
38368
  recipientCount: recipients.length,
37331
38369
  subject,
37332
38370
  bodyLength: body.length,
38371
+ bodyPreview: bodyPreview(body),
37333
38372
  threaded: Boolean(replyToMessageId || threadId),
37334
38373
  quoting: Boolean(quote),
37335
- attachmentCount: (attach?.length ?? 0) + (attachInline?.length ?? 0)
38374
+ attachmentCount: (attach?.length ?? 0) + (attachInline?.length ?? 0),
38375
+ attachments: attachmentNames(attach, attachInline)
38376
+ }, {
38377
+ tool: "gog_gmail_send",
38378
+ account,
38379
+ confirmToken,
38380
+ // Nothing is stored between the phases: the payload IS the arguments,
38381
+ // so phase 2 must repeat them and any difference is a changed send.
38382
+ subject: () => {
38383
+ const attachments = attachmentDetails(attach, attachInline);
38384
+ const from = senderPreview(account);
38385
+ return {
38386
+ target: replyToMessageId ?? threadId ?? "",
38387
+ payload: { from, to, cc, bcc, subject, body, attachments, threadId, inReplyTo: replyToMessageId, quote: Boolean(quote) },
38388
+ preview: {
38389
+ from,
38390
+ to,
38391
+ cc,
38392
+ bcc,
38393
+ subject,
38394
+ body,
38395
+ attachments: attachmentPreview(attachments),
38396
+ threadId,
38397
+ inReplyTo: replyToMessageId,
38398
+ quotesOriginal: Boolean(quote)
38399
+ }
38400
+ };
38401
+ }
37336
38402
  });
37337
38403
  if (confirmation) return confirmation;
37338
38404
  const result = await runOrDiagnose(args, { account });
@@ -37340,22 +38406,32 @@ function registerGmailTools(server) {
37340
38406
  return result;
37341
38407
  });
37342
38408
  server.registerTool("gog_gmail_reply", {
37343
- 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.',
38409
+ 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,
37344
38410
  annotations: { destructiveHint: true },
37345
38411
  inputSchema: sendReplySchema
37346
- }, async ({ messageId, account, ...flags }, ctx) => {
38412
+ }, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
37347
38413
  assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
37348
- return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx);
38414
+ confineReplyPaths(flags);
38415
+ return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx, confirmToken);
37349
38416
  });
37350
38417
  server.registerTool("gog_gmail_reply_all", {
37351
- 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.',
38418
+ 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,
37352
38419
  annotations: { destructiveHint: true },
37353
38420
  inputSchema: sendReplySchema
37354
- }, async ({ messageId, account, ...flags }, ctx) => {
38421
+ }, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
37355
38422
  assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
37356
- return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx);
38423
+ confineReplyPaths(flags);
38424
+ return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx, confirmToken);
38425
+ });
38426
+ registerRunTool(server, {
38427
+ service: "gmail",
38428
+ examples: '"archive", "mark-read", "labels"',
38429
+ // gog's --gmail-no-send blocks send/reply/forward/drafts send and all of
38430
+ // their aliases at runtime (verified on gog 0.41.0). Sending goes through the
38431
+ // confirmed tools, never this escape hatch.
38432
+ gmailNoSend: true,
38433
+ vet: vetGmailRun
37357
38434
  });
37358
- registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
37359
38435
  }
37360
38436
 
37361
38437
  // src/tools/sheets-a1.ts
@@ -37424,7 +38500,7 @@ function registerSheetsTools(server) {
37424
38500
  account: accountParam
37425
38501
  })
37426
38502
  }, async ({ spreadsheetId, range, account }) => {
37427
- return runOrDiagnose(["sheets", "get", spreadsheetId, range], { account });
38503
+ return runOrDiagnose(["sheets", "get", pos(spreadsheetId), pos(range)], { account });
37428
38504
  });
37429
38505
  server.registerTool("gog_sheets_update", {
37430
38506
  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)").',
@@ -37444,7 +38520,7 @@ function registerSheetsTools(server) {
37444
38520
  const readRange = expandAnchorRange(range, values.length, cols);
37445
38521
  let existing;
37446
38522
  try {
37447
- existing = await run(["sheets", "get", spreadsheetId, readRange], { account });
38523
+ existing = await run(["sheets", "get", pos(spreadsheetId), pos(readRange)], { account });
37448
38524
  } catch (err) {
37449
38525
  return diagnose(err);
37450
38526
  }
@@ -37456,7 +38532,7 @@ function registerSheetsTools(server) {
37456
38532
  );
37457
38533
  }
37458
38534
  }
37459
- const args = ["sheets", "update", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`];
38535
+ const args = ["sheets", "update", pos(spreadsheetId), pos(range), `--values-json=${JSON.stringify(values)}`];
37460
38536
  if (dry_run) args.push("--dry-run");
37461
38537
  if (fail_on_formula_error) args.push("--fail-on-formula-error");
37462
38538
  return runOrDiagnose(args, { account });
@@ -37472,7 +38548,7 @@ function registerSheetsTools(server) {
37472
38548
  account: accountParam
37473
38549
  })
37474
38550
  }, async ({ spreadsheetId, range, values, account, dry_run }) => {
37475
- const args = ["sheets", "append", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`];
38551
+ const args = ["sheets", "append", pos(spreadsheetId), pos(range), `--values-json=${JSON.stringify(values)}`];
37476
38552
  if (dry_run) args.push("--dry-run");
37477
38553
  return runOrDiagnose(args, { account });
37478
38554
  });
@@ -37486,7 +38562,7 @@ function registerSheetsTools(server) {
37486
38562
  account: accountParam
37487
38563
  })
37488
38564
  }, async ({ spreadsheetId, range, account, dry_run }) => {
37489
- const args = ["sheets", "clear", spreadsheetId, range];
38565
+ const args = ["sheets", "clear", pos(spreadsheetId), pos(range)];
37490
38566
  if (dry_run) args.push("--dry-run");
37491
38567
  return runOrDiagnose(args, { account });
37492
38568
  });
@@ -37498,7 +38574,7 @@ function registerSheetsTools(server) {
37498
38574
  account: accountParam
37499
38575
  })
37500
38576
  }, async ({ spreadsheetId, account }) => {
37501
- return runOrDiagnose(["sheets", "metadata", spreadsheetId], { account });
38577
+ return runOrDiagnose(["sheets", "metadata", pos(spreadsheetId)], { account });
37502
38578
  });
37503
38579
  server.registerTool("gog_sheets_create", {
37504
38580
  description: "Create a new Google Spreadsheet. Returns JSON with the new spreadsheetId and URL.",
@@ -37508,7 +38584,7 @@ function registerSheetsTools(server) {
37508
38584
  account: accountParam
37509
38585
  })
37510
38586
  }, async ({ title, account }) => {
37511
- return runOrDiagnose(["sheets", "create", title], { account });
38587
+ return runOrDiagnose(["sheets", "create", pos(title)], { account });
37512
38588
  });
37513
38589
  server.registerTool("gog_sheets_find_replace", {
37514
38590
  description: "Find and replace text across an entire Google Spreadsheet.",
@@ -37520,7 +38596,7 @@ function registerSheetsTools(server) {
37520
38596
  account: accountParam
37521
38597
  })
37522
38598
  }, async ({ spreadsheetId, find, replace, account }) => {
37523
- return runOrDiagnose(["sheets", "find-replace", spreadsheetId, find, replace], { account });
38599
+ return runOrDiagnose(["sheets", "find-replace", pos(spreadsheetId), pos(find), pos(replace)], { account });
37524
38600
  });
37525
38601
  registerRunTool(server, { service: "sheets", examples: '"freeze", "add-tab", "rename-tab"' });
37526
38602
  }
@@ -37528,8 +38604,9 @@ function registerSheetsTools(server) {
37528
38604
  // src/tools/slides.ts
37529
38605
  function registerSlidesTools(server) {
37530
38606
  server.registerTool("gog_slides_export", {
37531
- description: "Export a Google Slides presentation to a local file (pdf or pptx).",
37532
- annotations: { readOnlyHint: true },
38607
+ 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.",
38608
+ // Writes a file on the gog host and can overwrite one: not read-only.
38609
+ annotations: { readOnlyHint: false, destructiveHint: true },
37533
38610
  inputSchema: external_exports.object({
37534
38611
  presentationId: external_exports.string().describe("Presentation ID"),
37535
38612
  out: external_exports.string().optional().describe("Output file path"),
@@ -37538,7 +38615,8 @@ function registerSlidesTools(server) {
37538
38615
  account: accountParam
37539
38616
  })
37540
38617
  }, async ({ presentationId, out, format, overwrite, account }) => {
37541
- const args = ["slides", "export", presentationId];
38618
+ if (out) confinePath(out, "out");
38619
+ const args = ["slides", "export", pos(presentationId)];
37542
38620
  if (out) args.push(`--out=${out}`);
37543
38621
  if (format) args.push(`--format=${format}`);
37544
38622
  if (overwrite) args.push("--overwrite");
@@ -37552,7 +38630,7 @@ function registerSlidesTools(server) {
37552
38630
  account: accountParam
37553
38631
  })
37554
38632
  }, async ({ presentationId, account }) => {
37555
- return runOrDiagnose(["slides", "info", presentationId], { account });
38633
+ return runOrDiagnose(["slides", "info", pos(presentationId)], { account });
37556
38634
  });
37557
38635
  server.registerTool("gog_slides_create", {
37558
38636
  description: "Create a new Google Slides presentation, optionally in a folder or copying from a template.",
@@ -37564,7 +38642,7 @@ function registerSlidesTools(server) {
37564
38642
  account: accountParam
37565
38643
  })
37566
38644
  }, async ({ title, parent, template, account }) => {
37567
- const args = ["slides", "create", title];
38645
+ const args = ["slides", "create", pos(title)];
37568
38646
  if (parent) args.push(`--parent=${parent}`);
37569
38647
  if (template) args.push(`--template=${template}`);
37570
38648
  return runOrDiagnose(args, { account });
@@ -37579,7 +38657,7 @@ function registerSlidesTools(server) {
37579
38657
  account: accountParam
37580
38658
  })
37581
38659
  }, async ({ presentationId, title, parent, account }) => {
37582
- const args = ["slides", "copy", presentationId, title];
38660
+ const args = ["slides", "copy", pos(presentationId), pos(title)];
37583
38661
  if (parent) args.push(`--parent=${parent}`);
37584
38662
  return runOrDiagnose(args, { account });
37585
38663
  });
@@ -37591,7 +38669,7 @@ function registerSlidesTools(server) {
37591
38669
  account: accountParam
37592
38670
  })
37593
38671
  }, async ({ presentationId, account }) => {
37594
- return runOrDiagnose(["slides", "list-slides", presentationId], { account });
38672
+ return runOrDiagnose(["slides", "list-slides", pos(presentationId)], { account });
37595
38673
  });
37596
38674
  server.registerTool("gog_slides_read_slide", {
37597
38675
  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.",
@@ -37603,11 +38681,11 @@ function registerSlidesTools(server) {
37603
38681
  account: accountParam
37604
38682
  })
37605
38683
  }, async ({ presentationId, slideId, detail, account }) => {
37606
- const args = ["slides", "read-slide", presentationId, slideId];
38684
+ const args = ["slides", "read-slide", pos(presentationId), pos(slideId)];
37607
38685
  if (detail) args.push("--detail");
37608
38686
  return runOrDiagnose(args, { account });
37609
38687
  });
37610
- registerRunTool(server, { service: "slides", examples: '"add-slide", "delete-slide", "update-notes"' });
38688
+ registerRunTool(server, { service: "slides", examples: '"delete-slide", "update-notes"' });
37611
38689
  }
37612
38690
 
37613
38691
  // src/tools/tasks.ts
@@ -37629,7 +38707,7 @@ function registerTasksTools(server) {
37629
38707
  account: accountParam
37630
38708
  })
37631
38709
  }, async ({ tasklistId, account }) => {
37632
- return runOrDiagnose(["tasks", "list", tasklistId], { account });
38710
+ return runOrDiagnose(["tasks", "list", pos(tasklistId)], { account });
37633
38711
  });
37634
38712
  server.registerTool("gog_tasks_get", {
37635
38713
  description: "Get a specific task by ID.",
@@ -37640,7 +38718,7 @@ function registerTasksTools(server) {
37640
38718
  account: accountParam
37641
38719
  })
37642
38720
  }, async ({ tasklistId, taskId, account }) => {
37643
- return runOrDiagnose(["tasks", "get", tasklistId, taskId], { account });
38721
+ return runOrDiagnose(["tasks", "get", pos(tasklistId), pos(taskId)], { account });
37644
38722
  });
37645
38723
  server.registerTool("gog_tasks_add", {
37646
38724
  description: "Add a task to a Google Task list.",
@@ -37653,7 +38731,7 @@ function registerTasksTools(server) {
37653
38731
  account: accountParam
37654
38732
  })
37655
38733
  }, async ({ tasklistId, title, notes, due, account }) => {
37656
- const args = ["tasks", "add", tasklistId, `--title=${title}`];
38734
+ const args = ["tasks", "add", pos(tasklistId), `--title=${title}`];
37657
38735
  if (notes) args.push(`--notes=${notes}`);
37658
38736
  if (due) args.push(`--due=${due}`);
37659
38737
  return runOrDiagnose(args, { account });
@@ -37667,7 +38745,7 @@ function registerTasksTools(server) {
37667
38745
  account: accountParam
37668
38746
  })
37669
38747
  }, async ({ tasklistId, taskId, account }) => {
37670
- return runOrDiagnose(["tasks", "done", tasklistId, taskId], { account });
38748
+ return runOrDiagnose(["tasks", "done", pos(tasklistId), pos(taskId)], { account });
37671
38749
  });
37672
38750
  server.registerTool("gog_tasks_delete", {
37673
38751
  description: "Delete a task.",
@@ -37678,13 +38756,13 @@ function registerTasksTools(server) {
37678
38756
  account: accountParam
37679
38757
  })
37680
38758
  }, async ({ tasklistId, taskId, account }) => {
37681
- return runOrDiagnose(["tasks", "delete", tasklistId, taskId, "--force"], { account });
38759
+ return runOrDiagnose(["tasks", "delete", pos(tasklistId), pos(taskId), "--force"], { account });
37682
38760
  });
37683
38761
  registerRunTool(server, { service: "tasks", examples: '"update", "undo", "clear"' });
37684
38762
  }
37685
38763
 
37686
38764
  // src/server.ts
37687
- var VERSION = true ? "4.2.5" : "0.0.0";
38765
+ var VERSION = true ? "4.4.0" : "0.0.0";
37688
38766
  var BASE_TOOL_REGISTRARS = [
37689
38767
  registerApiTools,
37690
38768
  registerAppScriptTools,
@@ -37702,7 +38780,7 @@ var BASE_TOOL_REGISTRARS = [
37702
38780
  ];
37703
38781
 
37704
38782
  // src/bootstrap-auth.ts
37705
- import { join as join2 } from "node:path";
38783
+ import { join as join5 } from "node:path";
37706
38784
  var AUTH_BOOTSTRAP_MARKER = "auth-bootstrap.sha256";
37707
38785
  var VARS = ["GOG_CLIENT_ID", "GOG_CLIENT_SECRET", "GOG_REFRESH_TOKEN", "GOG_ACCOUNT"];
37708
38786
  var log = (line) => {
@@ -37736,12 +38814,12 @@ async function bootstrapGogAuth(env = process.env, options = {}) {
37736
38814
  const [clientId, clientSecret, refreshToken, account] = values;
37737
38815
  const { spawner } = options;
37738
38816
  try {
37739
- const { createHash } = await import("node:crypto");
38817
+ const { createHash: createHash4 } = await import("node:crypto");
37740
38818
  const { mkdir, readFile, writeFile, chmod } = await import("node:fs/promises");
37741
38819
  const home = options.home ?? (await import("node:os")).homedir();
37742
- const dir = join2(home, ".gogcli-mcp");
37743
- const marker = join2(dir, AUTH_BOOTSTRAP_MARKER);
37744
- const fingerprint = createHash("sha256").update(values.join("\0")).digest("hex");
38820
+ const dir = join5(home, ".gogcli-mcp");
38821
+ const marker = join5(dir, AUTH_BOOTSTRAP_MARKER);
38822
+ const fingerprint = createHash4("sha256").update(values.join("\0")).digest("hex");
37745
38823
  const previous = await readFile(marker, "utf8").catch(() => void 0);
37746
38824
  if (previous === fingerprint && await accountListed(account, spawner)) return "present";
37747
38825
  await run(