gogcli-mcp 4.3.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib.js CHANGED
@@ -33017,10 +33017,8 @@ function errorResult(message) {
33017
33017
 
33018
33018
  // ../../node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
33019
33019
  import { createHash, createHmac, timingSafeEqual } from "node:crypto";
33020
- var DEFAULT_REQUEST_KEY = "confirmation";
33021
- var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
33022
- var UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
33023
- var STATE_PREFIX = "mcpu.confirm.v1.";
33020
+
33021
+ // ../../node_modules/@chrischall/mcp-utils/dist/server/canonical.js
33024
33022
  function canonicalJson(value) {
33025
33023
  if (value === null || typeof value === "string" || typeof value === "boolean")
33026
33024
  return JSON.stringify(value);
@@ -33031,7 +33029,7 @@ function canonicalJson(value) {
33031
33029
  if (typeof value === "bigint")
33032
33030
  return `{"$bigint":"${value.toString()}"}`;
33033
33031
  if (typeof value === "function" || typeof value === "symbol") {
33034
- throw new TypeError(`requireConfirmation: cannot bind a ${typeof value} argument value.`);
33032
+ throw new TypeError(`confirmation: cannot bind a ${typeof value} argument value.`);
33035
33033
  }
33036
33034
  if (Array.isArray(value))
33037
33035
  return `[${value.map(canonicalJson).join(",")}]`;
@@ -33053,7 +33051,7 @@ function canonicalJson(value) {
33053
33051
  }
33054
33052
  const proto = Object.getPrototypeOf(value);
33055
33053
  if (proto !== Object.prototype && proto !== null) {
33056
- throw new TypeError("requireConfirmation: cannot bind a non-plain object argument value (class instance).");
33054
+ throw new TypeError("confirmation: cannot bind a non-plain object argument value (class instance).");
33057
33055
  }
33058
33056
  const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
33059
33057
  return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
@@ -33061,6 +33059,12 @@ function canonicalJson(value) {
33061
33059
  function escapeKey(key) {
33062
33060
  return key.startsWith("$") ? `$${key}` : key;
33063
33061
  }
33062
+
33063
+ // ../../node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
33064
+ var DEFAULT_REQUEST_KEY = "confirmation";
33065
+ var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
33066
+ 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";
33067
+ var STATE_PREFIX = "mcpu.confirm.v1.";
33064
33068
  function bindingKey(binding) {
33065
33069
  if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
33066
33070
  throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
@@ -33163,11 +33167,169 @@ ${JSON.stringify(preview, null, 2)}`,
33163
33167
  });
33164
33168
  }
33165
33169
 
33166
- // ../../node_modules/@chrischall/mcp-utils/dist/server/index.js
33167
- var SERVER_PROTOCOL_VERSIONS = Object.freeze([
33168
- "2026-07-28",
33169
- ...SUPPORTED_PROTOCOL_VERSIONS
33170
- ]);
33170
+ // ../../node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js
33171
+ import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
33172
+ function createSpentTokenStore() {
33173
+ const spent2 = /* @__PURE__ */ new Map();
33174
+ return {
33175
+ has: (nonce) => spent2.has(nonce),
33176
+ add: (nonce, exp) => {
33177
+ spent2.set(nonce, exp);
33178
+ },
33179
+ prune: (now) => {
33180
+ for (const [nonce, exp] of spent2)
33181
+ if (exp < now)
33182
+ spent2.delete(nonce);
33183
+ },
33184
+ clear: () => spent2.clear(),
33185
+ get size() {
33186
+ return spent2.size;
33187
+ }
33188
+ };
33189
+ }
33190
+ var PROCESS_SPENT = createSpentTokenStore();
33191
+ var PREFIX = "mcpu.token.v1.";
33192
+ var DEFAULT_TTL_SECONDS = 600;
33193
+ function tokenKey(key) {
33194
+ const bytes = typeof key === "string" ? Buffer.from(key, "utf8") : Buffer.from(key);
33195
+ if (bytes.length < 32)
33196
+ throw new RangeError("confirm token: key must be at least 32 bytes.");
33197
+ return bytes;
33198
+ }
33199
+ function ttlOf(ttlSeconds) {
33200
+ if (ttlSeconds === void 0)
33201
+ return DEFAULT_TTL_SECONDS;
33202
+ if (!(Number.isFinite(ttlSeconds) && ttlSeconds > 0)) {
33203
+ throw new RangeError("confirm token: ttlSeconds must be a finite number greater than 0.");
33204
+ }
33205
+ return ttlSeconds;
33206
+ }
33207
+ function sign(key, body) {
33208
+ return createHmac2("sha256", key).update(`${PREFIX}${body}`).digest();
33209
+ }
33210
+ function hashConfirmPayload(payload) {
33211
+ return createHash2("sha256").update(canonicalJson(payload)).digest("base64url");
33212
+ }
33213
+ function issueConfirmToken(key, binding, options = {}) {
33214
+ const k = tokenKey(key);
33215
+ const now = options.now ?? Date.now();
33216
+ const exp = now + ttlOf(options.ttlSeconds) * 1e3;
33217
+ const claims = {
33218
+ t: binding.tool,
33219
+ ...binding.account === void 0 ? {} : { a: binding.account },
33220
+ g: binding.target,
33221
+ ...binding.revision === void 0 ? {} : { r: binding.revision },
33222
+ h: binding.payloadHash,
33223
+ exp,
33224
+ n: randomBytes(16).toString("base64url")
33225
+ };
33226
+ const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
33227
+ return { token: `${PREFIX}${body}.${sign(k, body).toString("base64url")}`, expiresAt: new Date(exp).toISOString() };
33228
+ }
33229
+ function parseClaims(body) {
33230
+ try {
33231
+ const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
33232
+ return claims && typeof claims === "object" ? claims : void 0;
33233
+ } catch {
33234
+ return void 0;
33235
+ }
33236
+ }
33237
+ function verifyConfirmToken(key, token, binding, options = {}) {
33238
+ const k = tokenKey(key);
33239
+ const now = options.now ?? Date.now();
33240
+ const spent2 = options.spent ?? PROCESS_SPENT;
33241
+ spent2.prune(now);
33242
+ if (!token.startsWith(PREFIX))
33243
+ return { ok: false, error: "TOKEN_INVALID" };
33244
+ const rest = token.slice(PREFIX.length);
33245
+ const dot = rest.indexOf(".");
33246
+ const body = dot < 0 ? "" : rest.slice(0, dot);
33247
+ const mac3 = Buffer.from(dot < 0 ? "" : rest.slice(dot + 1), "base64url");
33248
+ const expected = sign(k, body);
33249
+ if (!body || mac3.length !== expected.length || !timingSafeEqual2(mac3, expected))
33250
+ return { ok: false, error: "TOKEN_INVALID" };
33251
+ const claims = parseClaims(body);
33252
+ if (!claims)
33253
+ return { ok: false, error: "TOKEN_INVALID" };
33254
+ if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
33255
+ return { ok: false, error: "TOKEN_INVALID" };
33256
+ }
33257
+ if (spent2.has(claims.n))
33258
+ return { ok: false, error: "TOKEN_REUSED" };
33259
+ if (now > claims.exp)
33260
+ return { ok: false, error: "TOKEN_EXPIRED" };
33261
+ if (claims.r !== binding.revision)
33262
+ return { ok: false, error: "DRAFT_CHANGED", reason: "revision-changed" };
33263
+ if (claims.h !== binding.payloadHash)
33264
+ return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
33265
+ spent2.add(claims.n, claims.exp);
33266
+ return { ok: true };
33267
+ }
33268
+ var CONFIRM_TOKEN_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
33269
+ var confirmTokenParam = external_exports.string().optional().describe(`ONLY for the two-step confirmation fallback (a client without MCP elicitation). The confirmToken from this same tool's phase-1 "confirmation-required" response, passed back ONLY after the user has seen that preview and explicitly approved it in chat \u2014 never on the first call, never invented, never reused. Call again with the same arguments. Ignored when the client supports elicitation.`);
33270
+ var TOKEN_ERROR_NOTE = {
33271
+ 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.",
33272
+ 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.",
33273
+ 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."
33274
+ };
33275
+ var CHANGED_NOTE = {
33276
+ "revision-changed": "Nothing was sent or changed: the target was edited since the user approved it (its version rotated), so what would happen is not what they saw.",
33277
+ "payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
33278
+ };
33279
+ function isToolResult(value) {
33280
+ return Array.isArray(value.content);
33281
+ }
33282
+ function rejection(data) {
33283
+ return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
33284
+ }
33285
+ async function tokenConfirmation(action, fb) {
33286
+ const subject = await fb.subject();
33287
+ if (isToolResult(subject))
33288
+ return subject;
33289
+ const binding = {
33290
+ tool: fb.tool,
33291
+ ...fb.account === void 0 ? {} : { account: fb.account },
33292
+ target: subject.target,
33293
+ ...subject.revision === void 0 ? {} : { revision: subject.revision },
33294
+ payloadHash: hashConfirmPayload(subject.payload)
33295
+ };
33296
+ const phaseOne = () => {
33297
+ const { token, expiresAt } = issueConfirmToken(fb.key, binding, { ttlSeconds: fb.ttlSeconds });
33298
+ return {
33299
+ action,
33300
+ preview: subject.preview,
33301
+ confirmToken: token,
33302
+ expiresAt,
33303
+ ttlSeconds: ttlOf(fb.ttlSeconds),
33304
+ instruction: fb.instruction ?? CONFIRM_TOKEN_INSTRUCTION
33305
+ };
33306
+ };
33307
+ if (!fb.confirmToken) {
33308
+ return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
33309
+ }
33310
+ const verdict = verifyConfirmToken(fb.key, fb.confirmToken, binding, { spent: fb.spent });
33311
+ if (verdict.ok)
33312
+ return void 0;
33313
+ if (verdict.error === "DRAFT_CHANGED") {
33314
+ return rejection({
33315
+ error: "DRAFT_CHANGED",
33316
+ reason: verdict.reason,
33317
+ note: `${CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
33318
+ ...phaseOne()
33319
+ });
33320
+ }
33321
+ return rejection({ error: verdict.error, action, note: TOKEN_ERROR_NOTE[verdict.error] });
33322
+ }
33323
+ async function requireConfirmationWithFallback(ctx, options) {
33324
+ const { tokenFallback, ...confirmation } = options;
33325
+ if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
33326
+ return tokenConfirmation(options.action, tokenFallback);
33327
+ }
33328
+ return requireConfirmation(ctx, confirmation);
33329
+ }
33330
+
33331
+ // ../../node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
33332
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
33171
33333
 
33172
33334
  // ../../node_modules/@chrischall/mcp-utils/dist/config/index.js
33173
33335
  import { homedir } from "node:os";
@@ -33208,6 +33370,67 @@ function expandPath(p) {
33208
33370
  return isAbsolute(expanded) ? expanded : resolve(expanded);
33209
33371
  }
33210
33372
 
33373
+ // ../../node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
33374
+ var MODES = /* @__PURE__ */ new Set(["ask-user", "auto", "refuse"]);
33375
+ var DEFAULT_TTL_SECONDS2 = 600;
33376
+ var warned = /* @__PURE__ */ new Set();
33377
+ function readConfirmMode(env = process.env) {
33378
+ const raw = readEnvVar("MCP_CONFIRM_MODE", { env })?.trim().toLowerCase();
33379
+ if (!raw)
33380
+ return "ask-user";
33381
+ if (MODES.has(raw))
33382
+ return raw;
33383
+ if (!warned.has(raw)) {
33384
+ warned.add(raw);
33385
+ process.stderr.write(`MCP_CONFIRM_MODE="${raw}" is not one of ask-user, auto, refuse; treating it as refuse.
33386
+ `);
33387
+ }
33388
+ return "refuse";
33389
+ }
33390
+ function confirmTtlFromEnv(env = process.env) {
33391
+ const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
33392
+ return raw && /^[1-9]\d*$/.test(raw) ? Number(raw) : DEFAULT_TTL_SECONDS2;
33393
+ }
33394
+ var processKey;
33395
+ function confirmKeyFromEnv(env = process.env) {
33396
+ const secret = readEnvVar("MCP_CONFIRM_SECRET", { env });
33397
+ if (secret)
33398
+ return createHash3("sha256").update(secret, "utf8").digest();
33399
+ processKey ??= randomBytes2(32);
33400
+ return processKey;
33401
+ }
33402
+ var CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
33403
+ var REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
33404
+ function confirmationFromEnv(options) {
33405
+ const { tool, account, confirmToken, subject, instruction, spent: spent2, env = process.env, ...confirmation } = options;
33406
+ const mode = readConfirmMode(env);
33407
+ if (mode === "refuse") {
33408
+ return {
33409
+ ...confirmation,
33410
+ unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${REFUSE_HINT}` : REFUSE_HINT
33411
+ };
33412
+ }
33413
+ return {
33414
+ ...confirmation,
33415
+ tokenFallback: {
33416
+ key: confirmKeyFromEnv(env),
33417
+ tool,
33418
+ ...account === void 0 ? {} : { account },
33419
+ ...confirmToken === void 0 ? {} : { confirmToken },
33420
+ subject,
33421
+ ttlSeconds: confirmTtlFromEnv(env),
33422
+ instruction: mode === "auto" ? CONFIRM_TOKEN_AUTO_INSTRUCTION : instruction ?? CONFIRM_TOKEN_INSTRUCTION,
33423
+ ...spent2 === void 0 ? {} : { spent: spent2 }
33424
+ }
33425
+ };
33426
+ }
33427
+
33428
+ // ../../node_modules/@chrischall/mcp-utils/dist/server/index.js
33429
+ var SERVER_PROTOCOL_VERSIONS = Object.freeze([
33430
+ "2026-07-28",
33431
+ ...SUPPORTED_PROTOCOL_VERSIONS
33432
+ ]);
33433
+
33211
33434
  // ../../node_modules/@chrischall/mcp-utils/dist/fs/confine.js
33212
33435
  import { existsSync, realpathSync } from "node:fs";
33213
33436
  import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
@@ -33289,9 +33512,9 @@ function offsetAt(instant, tz) {
33289
33512
  const w = wallPartsIn(instant, tz);
33290
33513
  const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
33291
33514
  const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
33292
- const sign2 = minutes < 0 ? "-" : "+";
33515
+ const sign3 = minutes < 0 ? "-" : "+";
33293
33516
  const abs = Math.abs(minutes);
33294
- return `${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
33517
+ return `${sign3}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
33295
33518
  }
33296
33519
  function wallPartsIn(instant, tz) {
33297
33520
  const parts = new Intl.DateTimeFormat("en-US", {
@@ -34199,13 +34422,108 @@ function assertNotBoth(inlineParam, fileParam, inlineValue, fileValue) {
34199
34422
  }
34200
34423
  }
34201
34424
 
34425
+ // src/dispatch-confirmation.ts
34426
+ import { createHash as createHash4 } from "node:crypto";
34427
+ import { readFileSync } from "node:fs";
34428
+
34429
+ // src/send-confirm-token.ts
34430
+ var spent = createSpentTokenStore();
34431
+ function confirmSpentStore() {
34432
+ return spent;
34433
+ }
34434
+
34435
+ // src/dispatch-confirmation.ts
34436
+ var BODY_PREVIEW_MAX = 2048;
34437
+ function bodyPreview(text) {
34438
+ if (!text) return void 0;
34439
+ if (text.length <= BODY_PREVIEW_MAX) return text;
34440
+ return `${text.slice(0, BODY_PREVIEW_MAX)}\u2026 [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
34441
+ }
34442
+ function attachmentNames(paths, inline) {
34443
+ return [...paths ?? [], ...(inline ?? []).map((a) => a.filename)];
34444
+ }
34445
+ function attachmentDetails(paths, inline) {
34446
+ const out = [];
34447
+ for (const path of paths ?? []) {
34448
+ let bytes;
34449
+ try {
34450
+ bytes = readFileSync(path);
34451
+ } catch {
34452
+ bytes = void 0;
34453
+ }
34454
+ out.push(bytes ? { name: path, size: bytes.length, sha256: createHash4("sha256").update(bytes).digest("hex") } : { name: path, size: null });
34455
+ }
34456
+ for (const a of inline ?? []) {
34457
+ out.push({
34458
+ name: a.filename,
34459
+ size: Buffer.from(a.contentBase64, "base64").length,
34460
+ sha256: createHash4("sha256").update(a.contentBase64).digest("hex")
34461
+ });
34462
+ }
34463
+ return out;
34464
+ }
34465
+ function senderPreview(account, from) {
34466
+ return from ?? account ?? readEnvVar("GOG_ACCOUNT") ?? "the gog account's default address";
34467
+ }
34468
+ function attachmentPreview(details) {
34469
+ return details.map(({ name, size }) => ({ name, size }));
34470
+ }
34471
+ 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.";
34472
+ var CONFIRM_ACTION_INSTRUCTION = CONFIRM_TOKEN_INSTRUCTION;
34473
+ var CONFIRM_FALLBACK_DESCRIPTION = ' If the client cannot show that prompt (no MCP elicitation, e.g. claude.ai), a two-step flow applies instead: call WITHOUT confirmToken and nothing is sent or changed \u2014 the result has status "confirmation-required", the full preview and a confirmToken. Follow its instruction (by default: show the preview to the user verbatim and only after they explicitly approve it in chat, call again with the SAME arguments plus confirmToken). The tool re-reads what it would act on and refuses (DRAFT_CHANGED, with a fresh preview and token) if it changed; TOKEN_EXPIRED / TOKEN_REUSED / TOKEN_INVALID also do nothing. A server set to MCP_CONFIRM_MODE=refuse refuses instead.';
34474
+ function gatedElsewhere(what, via, does, tool) {
34475
+ return `${what} ${does} and is not available through ${via}. Use ${tool}, which asks the user to confirm.`;
34476
+ }
34477
+ function hasCommandWord(args, words) {
34478
+ return args.find((a) => words.has(a.toLowerCase()));
34479
+ }
34480
+ async function requireDispatchConfirmation(ctx, options) {
34481
+ const { action, fallback } = options;
34482
+ const confirmation = {
34483
+ action,
34484
+ message: options.message,
34485
+ details: options.details,
34486
+ confirmationLabel: options.confirmationLabel,
34487
+ ...options.unsupportedNote ? { unsupportedNote: options.unsupportedNote } : {}
34488
+ };
34489
+ if (!fallback) return requireConfirmationWithFallback(ctx, confirmation);
34490
+ return requireConfirmationWithFallback(ctx, confirmationFromEnv({
34491
+ ...confirmation,
34492
+ tool: fallback.tool,
34493
+ account: fallback.account ?? readEnvVar("GOG_ACCOUNT") ?? "",
34494
+ confirmToken: fallback.confirmToken,
34495
+ subject: fallback.subject,
34496
+ instruction: fallback.instruction ?? CONFIRM_ACTION_INSTRUCTION,
34497
+ spent: confirmSpentStore()
34498
+ }));
34499
+ }
34500
+ function resultText(result) {
34501
+ const first = result.content[0];
34502
+ return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
34503
+ }
34504
+
34202
34505
  // src/tools/api.ts
34203
34506
  var GMAIL_API_BLOCKED = /(?:\.send$|forwarding|filters\.create|filters\.update|delegates\.create)/i;
34507
+ var DISPATCH_API_BLOCKED = {
34508
+ chat: [{ method: /(?:^|\.)spaces\.messages\.create$/i, does: "posts a Chat message", tool: "gog_chat_messages_send / gog_chat_dm_send" }],
34509
+ drive: [{ method: /(?:^|\.)permissions\.(?:create|update)$/i, does: "grants access to a file", tool: "gog_drive_share" }],
34510
+ classroom: [
34511
+ { method: /(?:^|\.)courses\.announcements\.create$/i, does: "posts to a class", tool: "gog_classroom_announcements_create" },
34512
+ { method: /(?:^|\.)invitations\.create$/i, does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
34513
+ ],
34514
+ calendar: [
34515
+ { method: /(?:^|\.)events\.(?:insert|import|quickadd)$/i, does: "can put an event on guests' calendars", tool: "gog_calendar_create" },
34516
+ { method: /(?:^|\.)events\.(?:update|patch)$/i, does: "can change what guests see", tool: "gog_calendar_update / gog_calendar_respond" }
34517
+ ]
34518
+ };
34204
34519
  function refusedApiCall(api, method) {
34205
- if (api.trim().toLowerCase() === "gmail" && GMAIL_API_BLOCKED.test(method.trim())) {
34520
+ const name = api.trim().toLowerCase();
34521
+ const m = method.trim();
34522
+ if (name === "gmail" && GMAIL_API_BLOCKED.test(m)) {
34206
34523
  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.`;
34207
34524
  }
34208
- return void 0;
34525
+ const hit = (Object.hasOwn(DISPATCH_API_BLOCKED, name) ? DISPATCH_API_BLOCKED[name] : []).find((b) => b.method.test(m));
34526
+ return hit ? gatedElsewhere(`${name} ${m}`, "gog_api_call", hit.does, hit.tool) : void 0;
34209
34527
  }
34210
34528
  function registerApiTools(server) {
34211
34529
  server.registerTool("gog_api_list", {
@@ -34235,7 +34553,7 @@ function registerApiTools(server) {
34235
34553
  return runOrDiagnose(args, { account });
34236
34554
  });
34237
34555
  server.registerTool("gog_api_call", {
34238
- 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.",
34556
+ 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.",
34239
34557
  annotations: { destructiveHint: true },
34240
34558
  inputSchema: external_exports.object({
34241
34559
  api: external_exports.string().describe("Discovery API name (e.g. drive, gmail, calendar)"),
@@ -34521,6 +34839,54 @@ function authToolsFor(defaultServices) {
34521
34839
  }
34522
34840
 
34523
34841
  // src/tools/calendar.ts
34842
+ var CALENDAR_GATED = {
34843
+ create: "gog_calendar_create",
34844
+ add: "gog_calendar_create",
34845
+ new: "gog_calendar_create",
34846
+ update: "gog_calendar_update",
34847
+ edit: "gog_calendar_update",
34848
+ set: "gog_calendar_update",
34849
+ respond: "gog_calendar_respond",
34850
+ rsvp: "gog_calendar_respond",
34851
+ reply: "gog_calendar_respond"
34852
+ };
34853
+ function vetCalendarRun(subcommand, _args) {
34854
+ const sub = subcommand.toLowerCase();
34855
+ const tool = Object.hasOwn(CALENDAR_GATED, sub) ? CALENDAR_GATED[sub] : void 0;
34856
+ return tool ? gatedElsewhere(`gog calendar ${sub}`, "gog_calendar_run", "can change what guests see", tool) : void 0;
34857
+ }
34858
+ function parseAttendees(value) {
34859
+ if (!value) return [];
34860
+ 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);
34861
+ }
34862
+ function timeOf(t) {
34863
+ const v = t?.dateTime ?? t?.date;
34864
+ return typeof v === "string" ? v : void 0;
34865
+ }
34866
+ function eventSnapshot(raw) {
34867
+ let event;
34868
+ try {
34869
+ event = JSON.parse(raw)?.event;
34870
+ } catch {
34871
+ event = void 0;
34872
+ }
34873
+ const str = (v) => typeof v === "string" ? v : void 0;
34874
+ return {
34875
+ summary: str(event?.summary),
34876
+ start: timeOf(event?.start),
34877
+ end: timeOf(event?.end),
34878
+ organizer: str(event?.organizer?.email),
34879
+ // Guests are the OTHER people: not this account's own attendee row, not rooms.
34880
+ guests: (Array.isArray(event?.attendees) ? event.attendees : []).filter((a) => a.self !== true && a.resource !== true && typeof a.email === "string").map((a) => a.email),
34881
+ etag: str(event?.etag)
34882
+ };
34883
+ }
34884
+ async function readEvent(calendarId, eventId, account) {
34885
+ const got = await runOrDiagnose(["calendar", "event", pos(calendarId), pos(eventId)], { account });
34886
+ if (got.isError) return { error: got };
34887
+ return { event: eventSnapshot(resultText(got)) };
34888
+ }
34889
+ var CALENDAR_CONFIRM_LABEL = "Confirm that this change, which other people will see, should be made now.";
34524
34890
  var reminderParams = {
34525
34891
  reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
34526
34892
  `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.`
@@ -34608,7 +34974,7 @@ function registerCalendarTools(server) {
34608
34974
  return runOrDiagnose(args, { account });
34609
34975
  });
34610
34976
  server.registerTool("gog_calendar_create", {
34611
- 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).",
34977
+ 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,
34612
34978
  annotations: { destructiveHint: false },
34613
34979
  inputSchema: external_exports.object({
34614
34980
  calendarId: external_exports.string().describe('Calendar ID (use "primary" for the default calendar)'),
@@ -34622,9 +34988,10 @@ function registerCalendarTools(server) {
34622
34988
  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."),
34623
34989
  withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)"),
34624
34990
  ...reminderParams,
34625
- account: accountParam
34991
+ account: accountParam,
34992
+ confirmToken: confirmTokenParam
34626
34993
  })
34627
- }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
34994
+ }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account, confirmToken }, ctx) => {
34628
34995
  const args = ["calendar", "create", pos(calendarId), `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
34629
34996
  if (description) args.push(`--description=${description}`);
34630
34997
  if (location) args.push(`--location=${location}`);
@@ -34633,10 +35000,28 @@ function registerCalendarTools(server) {
34633
35000
  if (timezone) args.push(`--timezone=${timezone}`);
34634
35001
  if (withZoom) args.push("--with-zoom");
34635
35002
  pushReminderFlags(args, { reminders, noReminders });
35003
+ const guests = parseAttendees(attendees);
35004
+ if (guests.length > 0) {
35005
+ const event = { calendarId, summary, from, to, allDay: Boolean(allDay), timezone, location, description, guests, attendees, withZoom: Boolean(withZoom) };
35006
+ const confirmation = await requireDispatchConfirmation(ctx, {
35007
+ action: "calendar.create",
35008
+ message: "Review and confirm this event, which will appear on the guests' calendars:",
35009
+ confirmationLabel: CALENDAR_CONFIRM_LABEL,
35010
+ details: event,
35011
+ unsupportedNote: "Create it without attendees instead; the user can add the guests in Google Calendar.",
35012
+ fallback: {
35013
+ tool: "gog_calendar_create",
35014
+ account,
35015
+ confirmToken,
35016
+ subject: () => ({ target: calendarId, payload: event, preview: event })
35017
+ }
35018
+ });
35019
+ if (confirmation) return confirmation;
35020
+ }
34636
35021
  return runOrDiagnose(args, { account });
34637
35022
  });
34638
35023
  server.registerTool("gog_calendar_update", {
34639
- 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.",
35024
+ 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,
34640
35025
  annotations: { destructiveHint: false },
34641
35026
  inputSchema: external_exports.object({
34642
35027
  calendarId: external_exports.string().describe("Calendar ID"),
@@ -34654,9 +35039,10 @@ function registerCalendarTools(server) {
34654
35039
  removeZoom: external_exports.boolean().optional().describe("Remove the event's Zoom video conference"),
34655
35040
  removeMeet: external_exports.boolean().optional().describe("Remove the event's Google Meet video conference (clears conference data only)"),
34656
35041
  ...reminderParams,
34657
- account: accountParam
35042
+ account: accountParam,
35043
+ confirmToken: confirmTokenParam
34658
35044
  })
34659
- }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
35045
+ }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account, confirmToken }, ctx) => {
34660
35046
  const args = ["calendar", "update", pos(calendarId), pos(eventId)];
34661
35047
  if (summary !== void 0) args.push(`--summary=${summary}`);
34662
35048
  if (from !== void 0) args.push(`--from=${from}`);
@@ -34671,6 +35057,45 @@ function registerCalendarTools(server) {
34671
35057
  if (removeZoom) args.push("--remove-zoom");
34672
35058
  if (removeMeet) args.push("--remove-meet");
34673
35059
  pushReminderFlags(args, { reminders, noReminders });
35060
+ const changes = Object.fromEntries(Object.entries({
35061
+ summary,
35062
+ from,
35063
+ to,
35064
+ description,
35065
+ location,
35066
+ attendees,
35067
+ addAttendees,
35068
+ attachments,
35069
+ withZoom,
35070
+ regenerateZoom,
35071
+ removeZoom,
35072
+ removeMeet
35073
+ }).filter(([, v]) => v !== void 0 && v !== false));
35074
+ if (Object.keys(changes).length > 0) {
35075
+ const read = await readEvent(calendarId, eventId, account);
35076
+ if (read.error) return read.error;
35077
+ const { etag, ...current } = read.event;
35078
+ const added = [...parseAttendees(attendees), ...parseAttendees(addAttendees)];
35079
+ if (current.guests.length > 0 || added.length > 0) {
35080
+ const view = { calendarId, eventId, current, changes };
35081
+ const confirmation = await requireDispatchConfirmation(ctx, {
35082
+ action: "calendar.update",
35083
+ message: "Review and confirm this change to an event other people are on:",
35084
+ confirmationLabel: CALENDAR_CONFIRM_LABEL,
35085
+ details: view,
35086
+ unsupportedNote: "Ask the user to make this change in Google Calendar.",
35087
+ fallback: {
35088
+ tool: "gog_calendar_update",
35089
+ account,
35090
+ confirmToken,
35091
+ // The etag rotates on any edit, so an event changed elsewhere between
35092
+ // the two phases is DRAFT_CHANGED rather than edited over.
35093
+ subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
35094
+ }
35095
+ });
35096
+ if (confirmation) return confirmation;
35097
+ }
35098
+ }
34674
35099
  return runOrDiagnose(args, { account });
34675
35100
  });
34676
35101
  server.registerTool("gog_calendar_delete", {
@@ -34685,21 +35110,40 @@ function registerCalendarTools(server) {
34685
35110
  return runOrDiagnose(["calendar", "delete", pos(calendarId), pos(eventId), "--force"], { account });
34686
35111
  });
34687
35112
  server.registerTool("gog_calendar_respond", {
34688
- description: "Respond to a calendar event invitation.",
35113
+ 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,
34689
35114
  annotations: { destructiveHint: true },
34690
35115
  inputSchema: external_exports.object({
34691
35116
  calendarId: external_exports.string().describe("Calendar ID"),
34692
35117
  eventId: external_exports.string().describe("Event ID"),
34693
35118
  status: external_exports.enum(["accepted", "declined", "tentative"]).describe("Response status"),
34694
35119
  comment: external_exports.string().optional().describe("Optional comment to include with response"),
34695
- account: accountParam
35120
+ account: accountParam,
35121
+ confirmToken: confirmTokenParam
34696
35122
  })
34697
- }, async ({ calendarId, eventId, status, comment, account }) => {
35123
+ }, async ({ calendarId, eventId, status, comment, account, confirmToken }, ctx) => {
34698
35124
  const args = ["calendar", "respond", pos(calendarId), pos(eventId), `--status=${status}`];
34699
35125
  if (comment) args.push(`--comment=${comment}`);
35126
+ const read = await readEvent(calendarId, eventId, account);
35127
+ if (read.error) return read.error;
35128
+ const { etag, ...event } = read.event;
35129
+ const view = { calendarId, eventId, event, response: status, ...comment ? { comment } : {} };
35130
+ const confirmation = await requireDispatchConfirmation(ctx, {
35131
+ action: "calendar.respond",
35132
+ message: "Review and confirm this response, which the organizer will see:",
35133
+ confirmationLabel: "Confirm that this response should be recorded now.",
35134
+ details: view,
35135
+ unsupportedNote: "Ask the user to respond from Google Calendar.",
35136
+ fallback: {
35137
+ tool: "gog_calendar_respond",
35138
+ account,
35139
+ confirmToken,
35140
+ subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
35141
+ }
35142
+ });
35143
+ if (confirmation) return confirmation;
34700
35144
  return runOrDiagnose(args, { account });
34701
35145
  });
34702
- registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"' });
35146
+ registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"', vet: vetCalendarRun });
34703
35147
  }
34704
35148
 
34705
35149
  // src/attachments.ts
@@ -34783,6 +35227,23 @@ function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
34783
35227
  }
34784
35228
 
34785
35229
  // src/tools/chat.ts
35230
+ var CHAT_SEND_WORDS = /* @__PURE__ */ new Set(["send", "create", "post"]);
35231
+ function vetChatRun(subcommand, args) {
35232
+ const sub = subcommand.toLowerCase();
35233
+ if (sub !== "messages" && sub !== "dm") return void 0;
35234
+ const word = hasCommandWord(args, CHAT_SEND_WORDS);
35235
+ if (!word) return void 0;
35236
+ return gatedElsewhere(
35237
+ `gog chat ${sub} ${word.toLowerCase()}`,
35238
+ "gog_chat_run",
35239
+ "posts a message",
35240
+ sub === "dm" ? "gog_chat_dm_send" : "gog_chat_messages_send"
35241
+ );
35242
+ }
35243
+ var CHAT_CONFIRM = {
35244
+ message: "Review and confirm this Google Chat message:",
35245
+ confirmationLabel: "Confirm that this Chat message should be posted now."
35246
+ };
34786
35247
  function registerChatTools(server) {
34787
35248
  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.';
34788
35249
  const spaceParam = external_exports.string().describe(
@@ -34888,7 +35349,7 @@ function registerChatTools(server) {
34888
35349
  return runOrDiagnose(args, { account });
34889
35350
  });
34890
35351
  server.registerTool("gog_chat_messages_send", {
34891
- 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,
35352
+ 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,
34892
35353
  annotations: { destructiveHint: true },
34893
35354
  inputSchema: external_exports.object({
34894
35355
  space: spaceParam,
@@ -34898,9 +35359,10 @@ function registerChatTools(server) {
34898
35359
  "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)."
34899
35360
  ),
34900
35361
  attachInline: attachInlineParam,
34901
- account: accountParam
35362
+ account: accountParam,
35363
+ confirmToken: confirmTokenParam
34902
35364
  })
34903
- }, async ({ space, text, thread, attach, attachInline, account }) => {
35365
+ }, async ({ space, text, thread, attach, attachInline, account, confirmToken }, ctx) => {
34904
35366
  confinePaths(attach, "attach");
34905
35367
  if (text === void 0 && !attach?.length && !attachInline?.length) {
34906
35368
  throw new Error("A Chat message needs text, an attachment, or both.");
@@ -34910,20 +35372,56 @@ function registerChatTools(server) {
34910
35372
  if (thread) args.push(`--thread=${thread}`);
34911
35373
  if (attach) for (const path of attach) args.push(`--attach=${path}`);
34912
35374
  args.push(...inlineAttachmentArgs("attach", attachInline, args));
35375
+ const confirmation = await requireDispatchConfirmation(ctx, {
35376
+ ...CHAT_CONFIRM,
35377
+ action: "chat.message-send",
35378
+ details: { space, thread, textPreview: bodyPreview(text), attachments: attachmentNames(attach, attachInline) },
35379
+ fallback: {
35380
+ tool: "gog_chat_messages_send",
35381
+ account,
35382
+ confirmToken,
35383
+ subject: () => {
35384
+ const attachments = attachmentDetails(attach, attachInline);
35385
+ const from = senderPreview(account);
35386
+ return {
35387
+ target: space,
35388
+ payload: { from, space, thread, text, attachments },
35389
+ preview: { from, space, thread, text, attachments: attachmentPreview(attachments) }
35390
+ };
35391
+ }
35392
+ }
35393
+ });
35394
+ if (confirmation) return confirmation;
34913
35395
  return runOrDiagnose(args, { account });
34914
35396
  });
34915
35397
  server.registerTool("gog_chat_dm_send", {
34916
- 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,
35398
+ 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,
34917
35399
  annotations: { destructiveHint: true },
34918
35400
  inputSchema: external_exports.object({
34919
35401
  email: external_exports.string().describe("Recipient email address"),
34920
35402
  text: external_exports.string().describe("Message text"),
34921
35403
  thread: threadParam,
34922
- account: accountParam
35404
+ account: accountParam,
35405
+ confirmToken: confirmTokenParam
34923
35406
  })
34924
- }, async ({ email: email3, text, thread, account }) => {
35407
+ }, async ({ email: email3, text, thread, account, confirmToken }, ctx) => {
34925
35408
  const args = ["chat", "dm", "send", pos(email3), `--text=${text}`];
34926
35409
  if (thread) args.push(`--thread=${thread}`);
35410
+ const confirmation = await requireDispatchConfirmation(ctx, {
35411
+ ...CHAT_CONFIRM,
35412
+ action: "chat.dm-send",
35413
+ details: { to: email3, thread, textPreview: bodyPreview(text) },
35414
+ fallback: {
35415
+ tool: "gog_chat_dm_send",
35416
+ account,
35417
+ confirmToken,
35418
+ subject: () => {
35419
+ const from = senderPreview(account);
35420
+ return { target: email3, payload: { from, to: email3, thread, text }, preview: { from, to: email3, thread, text } };
35421
+ }
35422
+ }
35423
+ });
35424
+ if (confirmation) return confirmation;
34927
35425
  return runOrDiagnose(args, { account });
34928
35426
  });
34929
35427
  server.registerTool("gog_chat_dm_space", {
@@ -34978,11 +35476,45 @@ function registerChatTools(server) {
34978
35476
  registerRunTool(server, {
34979
35477
  service: "chat",
34980
35478
  examples: '"spaces", "messages", "dm"',
35479
+ vet: vetChatRun,
34981
35480
  note: "Google Chat has no API for consumer accounts: every chat subcommand fails on an @gmail.com account regardless of scopes."
34982
35481
  });
34983
35482
  }
34984
35483
 
34985
35484
  // src/tools/classroom.ts
35485
+ var CLASSROOM_CREATE_WORDS = /* @__PURE__ */ new Set(["create", "add", "new"]);
35486
+ var CLASSROOM_GATED = {
35487
+ announcements: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
35488
+ announcement: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
35489
+ ann: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
35490
+ invitations: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
35491
+ invitation: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
35492
+ invites: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
35493
+ };
35494
+ function vetClassroomRun(subcommand, args) {
35495
+ const sub = subcommand.toLowerCase();
35496
+ const gated = Object.hasOwn(CLASSROOM_GATED, sub) ? CLASSROOM_GATED[sub] : void 0;
35497
+ if (!gated) return void 0;
35498
+ const word = hasCommandWord(args, CLASSROOM_CREATE_WORDS);
35499
+ return word ? gatedElsewhere(`gog classroom ${sub} ${word.toLowerCase()}`, "gog_classroom_run", gated.does, gated.tool) : void 0;
35500
+ }
35501
+ async function readCourse(courseId, account, runner = runOrDiagnose) {
35502
+ const got = await runner(["classroom", "courses", "get", pos(courseId)], { account });
35503
+ if (got.isError) return { error: got };
35504
+ let course;
35505
+ try {
35506
+ course = JSON.parse(resultText(got))?.course;
35507
+ } catch {
35508
+ course = void 0;
35509
+ }
35510
+ return {
35511
+ course: {
35512
+ id: courseId,
35513
+ ...typeof course?.name === "string" ? { name: course.name } : {},
35514
+ ...typeof course?.section === "string" ? { section: course.section } : {}
35515
+ }
35516
+ };
35517
+ }
34986
35518
  function registerClassroomTools(server) {
34987
35519
  server.registerTool("gog_classroom_courses_list", {
34988
35520
  description: "List Google Classroom courses.",
@@ -35265,19 +35797,42 @@ function registerClassroomTools(server) {
35265
35797
  return runOrDiagnose(["classroom", "announcements", "get", pos(courseId), pos(announcementId)], { account });
35266
35798
  });
35267
35799
  server.registerTool("gog_classroom_announcements_create", {
35268
- description: "Create an announcement in a Google Classroom course.",
35800
+ 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,
35269
35801
  annotations: { destructiveHint: true },
35270
35802
  inputSchema: external_exports.object({
35271
35803
  courseId: external_exports.string().describe("Course ID"),
35272
35804
  text: external_exports.string().describe("Announcement text"),
35273
- state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State"),
35805
+ state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State (DRAFT is visible only to teachers and needs no confirmation)"),
35274
35806
  scheduled: external_exports.string().optional().describe("Scheduled publish time"),
35275
- account: accountParam
35807
+ account: accountParam,
35808
+ confirmToken: confirmTokenParam
35276
35809
  })
35277
- }, async ({ courseId, text, state, scheduled, account }) => {
35810
+ }, async ({ courseId, text, state, scheduled, account, confirmToken }, ctx) => {
35278
35811
  const args = ["classroom", "announcements", "create", pos(courseId), `--text=${text}`];
35279
35812
  if (state) args.push(`--state=${state}`);
35280
35813
  if (scheduled) args.push(`--scheduled=${scheduled}`);
35814
+ if (state !== "DRAFT") {
35815
+ const read = await readCourse(courseId, account);
35816
+ if (read.error) return read.error;
35817
+ const publishes = scheduled ? `at ${scheduled}` : "immediately";
35818
+ const confirmation = await requireDispatchConfirmation(ctx, {
35819
+ action: "classroom.announcement-create",
35820
+ message: "Review and confirm this Classroom announcement:",
35821
+ confirmationLabel: "Confirm that this announcement should be posted to the class.",
35822
+ details: { course: read.course, publishes, textPreview: bodyPreview(text) },
35823
+ unsupportedNote: "Create it with state DRAFT instead; the user can review and post it from Classroom.",
35824
+ fallback: {
35825
+ tool: "gog_classroom_announcements_create",
35826
+ account,
35827
+ confirmToken,
35828
+ subject: () => {
35829
+ const view = { course: read.course, publishes, text };
35830
+ return { target: courseId, payload: view, preview: view };
35831
+ }
35832
+ }
35833
+ });
35834
+ if (confirmation) return confirmation;
35835
+ }
35281
35836
  return runOrDiagnose(args, { account });
35282
35837
  });
35283
35838
  server.registerTool("gog_classroom_topics_list", {
@@ -35367,6 +35922,7 @@ function registerClassroomTools(server) {
35367
35922
  registerRunTool(server, {
35368
35923
  service: "classroom",
35369
35924
  examples: '"guardians", "materials", "guardian-invitations"',
35925
+ vet: vetClassroomRun,
35370
35926
  note: "Covers anything not wrapped by the dedicated tools (guardians, guardian-invitations, materials, coursework assignees, announcement assignees, etc.)."
35371
35927
  });
35372
35928
  }
@@ -35527,7 +36083,18 @@ function registerDocsTools(server) {
35527
36083
  }
35528
36084
 
35529
36085
  // src/tools/drive.ts
36086
+ function vetDriveRun(subcommand, _args) {
36087
+ return subcommand.toLowerCase() === "share" ? gatedElsewhere("gog drive share", "gog_drive_run", "grants access to a file", "gog_drive_share") : void 0;
36088
+ }
35530
36089
  var GOOGLE_DOC_MIME = "application/vnd.google-apps.document";
36090
+ function shareTargetMeta(raw) {
36091
+ try {
36092
+ const { name, mimeType } = fileMeta(raw);
36093
+ return { name, mimeType };
36094
+ } catch {
36095
+ return {};
36096
+ }
36097
+ }
35531
36098
  function fileMeta(raw) {
35532
36099
  const parsed = JSON.parse(raw);
35533
36100
  const f = parsed.file ?? parsed;
@@ -35650,7 +36217,7 @@ function registerDriveTools(server) {
35650
36217
  return runOrDiagnose(args, { account });
35651
36218
  });
35652
36219
  server.registerTool("gog_drive_share", {
35653
- description: "Share a Google Drive file or folder.",
36220
+ 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,
35654
36221
  annotations: { destructiveHint: true },
35655
36222
  inputSchema: external_exports.object({
35656
36223
  fileId: external_exports.string().describe("File or folder ID"),
@@ -35658,9 +36225,35 @@ function registerDriveTools(server) {
35658
36225
  email: external_exports.string().optional().describe("User email (required when to=user)"),
35659
36226
  domain: external_exports.string().optional().describe("Domain (required when to=domain)"),
35660
36227
  role: external_exports.enum(["reader", "writer"]).optional().describe("Permission role (default: reader)"),
35661
- account: accountParam
36228
+ account: accountParam,
36229
+ confirmToken: confirmTokenParam
35662
36230
  })
35663
- }, async ({ fileId, to, email: email3, domain: domain2, role, account }) => {
36231
+ }, async ({ fileId, to, email: email3, domain: domain2, role, account, confirmToken }, ctx) => {
36232
+ const got = await runOrDiagnose(["drive", "get", pos(fileId)], { account });
36233
+ if (got.isError) return got;
36234
+ const file2 = { id: fileId, ...shareTargetMeta(resultText(got)) };
36235
+ const grant = {
36236
+ file: file2,
36237
+ to,
36238
+ ...email3 ? { email: email3 } : {},
36239
+ ...domain2 ? { domain: domain2 } : {},
36240
+ role: role ?? "reader",
36241
+ publicLink: to === "anyone"
36242
+ };
36243
+ const confirmation = await requireDispatchConfirmation(ctx, {
36244
+ action: "drive.share",
36245
+ message: to === "anyone" ? "Review and confirm making this file available to ANYONE with the link:" : "Review and confirm this Drive share:",
36246
+ confirmationLabel: "Confirm that this access should be granted now.",
36247
+ details: grant,
36248
+ unsupportedNote: "Ask the user to share it themselves from Google Drive.",
36249
+ fallback: {
36250
+ tool: "gog_drive_share",
36251
+ account,
36252
+ confirmToken,
36253
+ subject: () => ({ target: fileId, payload: grant, preview: grant })
36254
+ }
36255
+ });
36256
+ if (confirmation) return confirmation;
35664
36257
  const args = ["drive", "share", pos(fileId), `--to=${to}`];
35665
36258
  if (email3) args.push(`--email=${email3}`);
35666
36259
  if (domain2) args.push(`--domain=${domain2}`);
@@ -35773,7 +36366,7 @@ function registerDriveTools(server) {
35773
36366
  return diagnose(err);
35774
36367
  }
35775
36368
  });
35776
- registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"' });
36369
+ registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"', vet: vetDriveRun });
35777
36370
  }
35778
36371
 
35779
36372
  // src/gmail-results.ts
@@ -35899,24 +36492,16 @@ var STAGING_TWIN = {
35899
36492
  var UNSUPPORTED_NOTE2 = {
35900
36493
  "gmail.drafts-send": "The draft is still saved: ask the user to review it and send it from Gmail."
35901
36494
  };
35902
- var BODY_PREVIEW_MAX = 2048;
35903
- function bodyPreview(text) {
35904
- if (!text) return void 0;
35905
- if (text.length <= BODY_PREVIEW_MAX) return text;
35906
- return `${text.slice(0, BODY_PREVIEW_MAX)}\u2026 [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
35907
- }
35908
- function attachmentNames(paths, inline) {
35909
- return [...paths ?? [], ...(inline ?? []).map((a) => a.filename)];
35910
- }
35911
- function requireGmailDispatchConfirmation(ctx, op, details) {
36495
+ async function requireGmailDispatchConfirmation(ctx, op, details, fallback) {
35912
36496
  const twin = STAGING_TWIN[op];
35913
36497
  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];
35914
- return requireConfirmation(ctx, {
36498
+ return requireDispatchConfirmation(ctx, {
35915
36499
  action: op,
35916
36500
  message: op === "gmail.filter-forward" ? "Review and confirm this mail-forwarding filter:" : "Review and confirm this email dispatch:",
35917
36501
  details,
35918
36502
  confirmationLabel: op === "gmail.filter-forward" ? "Confirm that matching mail should be forwarded automatically from now on." : "Confirm that this email should be sent now.",
35919
- ...note ? { unsupportedNote: note } : {}
36503
+ unsupportedNote: note,
36504
+ ...fallback ? { fallback: { instruction: CONFIRM_SEND_INSTRUCTION, ...fallback } } : {}
35920
36505
  });
35921
36506
  }
35922
36507
  var EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/gi;
@@ -35970,10 +36555,6 @@ function logGmailDispatch(tool, recipients, account) {
35970
36555
  process.stderr.write(`${JSON.stringify(event)}
35971
36556
  `);
35972
36557
  }
35973
- function resultText(result) {
35974
- const first = result.content[0];
35975
- return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
35976
- }
35977
36558
 
35978
36559
  // src/tools/gmail.ts
35979
36560
  var replySchema = {
@@ -36021,7 +36602,7 @@ function appendReplyFlags(args, f) {
36021
36602
  if (f.signatureFile) args.push(`--signature-file=${f.signatureFile}`);
36022
36603
  args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
36023
36604
  }
36024
- var sendReplySchema = external_exports.object(replySchema);
36605
+ var sendReplySchema = external_exports.object({ ...replySchema, confirmToken: confirmTokenParam });
36025
36606
  function parseMetadataHeaders(raw) {
36026
36607
  let parsed;
36027
36608
  try {
@@ -36046,12 +36627,69 @@ function computeReplyRecipients(kind, headers, flags) {
36046
36627
  }
36047
36628
  return emails;
36048
36629
  }
36049
- async function sendReply(kind, toolName, messageId, account, flags, ctx) {
36630
+ function metadataThreadId(raw) {
36631
+ try {
36632
+ const threadId = JSON.parse(raw)?.message?.threadId;
36633
+ return typeof threadId === "string" ? threadId : void 0;
36634
+ } catch {
36635
+ return void 0;
36636
+ }
36637
+ }
36638
+ function replyTokenSubject(kind, messageId, account, headers, threadId, flags) {
36639
+ const removed = new Set(extractEmails(...flags.remove ?? []));
36640
+ const keep = (emails) => emails.filter((e) => !removed.has(e));
36641
+ const to = keep(extractEmails(headers.from, ...kind === "reply-all" ? [headers.to] : [], ...flags.to ?? []));
36642
+ const cc = keep(extractEmails(...kind === "reply-all" ? [headers.cc] : [], ...flags.cc ?? []));
36643
+ const bcc = keep(extractEmails(...flags.bcc ?? []));
36644
+ const attachments = attachmentDetails(flags.attach, flags.attachInline);
36645
+ const subject = flags.subject || (headers.subject ? `Re: ${headers.subject}` : void 0);
36646
+ const from = flags.from ?? (flags.autoFromAddressedAlias ? "the send-as alias the original was addressed to" : senderPreview(account));
36647
+ const localFiles = attachmentDetails([flags.bodyHtmlFile, flags.signatureFile].filter((p) => Boolean(p)), void 0);
36648
+ return {
36649
+ target: messageId,
36650
+ payload: {
36651
+ kind,
36652
+ to,
36653
+ cc,
36654
+ bcc,
36655
+ subject,
36656
+ from,
36657
+ body: flags.body,
36658
+ bodyHtml: flags.bodyHtml,
36659
+ localFiles,
36660
+ signature: Boolean(flags.signature),
36661
+ signatureFrom: flags.signatureFrom,
36662
+ attachments,
36663
+ quote: !flags.noQuote,
36664
+ threadId,
36665
+ inReplyTo: headers.message_id,
36666
+ references: headers.references
36667
+ },
36668
+ preview: {
36669
+ from,
36670
+ to,
36671
+ cc,
36672
+ bcc,
36673
+ subject,
36674
+ body: flags.body,
36675
+ ...flags.bodyHtml ? { bodyHtml: flags.bodyHtml } : {},
36676
+ ...flags.bodyHtmlFile ? { bodyHtmlFile: flags.bodyHtmlFile } : {},
36677
+ ...flags.signature || flags.signatureFrom || flags.signatureFile ? { signature: flags.signatureFile ?? flags.signatureFrom ?? "the Gmail signature of the sending address" } : {},
36678
+ quotesOriginal: !flags.noQuote,
36679
+ attachments: attachmentPreview(attachments),
36680
+ replyingToMessageId: messageId,
36681
+ threadId,
36682
+ inReplyTo: headers.message_id
36683
+ }
36684
+ };
36685
+ }
36686
+ async function sendReply(kind, toolName, messageId, account, flags, ctx, confirmToken) {
36050
36687
  const metaResult = await runOrDiagnose(["gmail", "get", pos(messageId), "--format=metadata"], { account });
36051
36688
  if (metaResult.isError) return metaResult;
36052
- const headers = parseMetadataHeaders(resultText(metaResult));
36689
+ const metaText = resultText(metaResult);
36690
+ const headers = parseMetadataHeaders(metaText);
36053
36691
  const recipients = computeReplyRecipients(kind, headers, flags);
36054
- const confirmation = requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
36692
+ const confirmation = await requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
36055
36693
  messageId,
36056
36694
  recipients,
36057
36695
  recipientCount: recipients.length,
@@ -36062,6 +36700,13 @@ async function sendReply(kind, toolName, messageId, account, flags, ctx) {
36062
36700
  bodyHtmlFile: flags.bodyHtmlFile,
36063
36701
  attachmentCount: (flags.attach?.length ?? 0) + (flags.attachInline?.length ?? 0),
36064
36702
  attachments: attachmentNames(flags.attach, flags.attachInline)
36703
+ }, {
36704
+ tool: toolName,
36705
+ account,
36706
+ confirmToken,
36707
+ // The metadata read above IS the phase-2 re-read: it runs on every call,
36708
+ // so the original's Message-ID/References are fresh here.
36709
+ subject: () => replyTokenSubject(kind, messageId, account, headers, metadataThreadId(metaText), flags)
36065
36710
  });
36066
36711
  if (confirmation) return confirmation;
36067
36712
  const args = ["gmail", kind, pos(messageId)];
@@ -36079,7 +36724,7 @@ function vetGmailRun(subcommand, args) {
36079
36724
  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.`;
36080
36725
  }
36081
36726
  if (subcommand === "settings") {
36082
- const blocked = args.find((a) => GMAIL_RUN_BLOCKED_SETTINGS.has(a.toLowerCase()));
36727
+ const blocked = hasCommandWord(args, GMAIL_RUN_BLOCKED_SETTINGS);
36083
36728
  if (blocked) {
36084
36729
  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.`;
36085
36730
  }
@@ -36138,7 +36783,7 @@ function registerGmailTools(server) {
36138
36783
  return runOrDiagnose(args, { account });
36139
36784
  });
36140
36785
  server.registerTool("gog_gmail_send", {
36141
- 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.',
36786
+ 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,
36142
36787
  annotations: { destructiveHint: true },
36143
36788
  inputSchema: external_exports.object({
36144
36789
  to: external_exports.string().describe("Recipient(s), comma-separated"),
@@ -36151,9 +36796,10 @@ function registerGmailTools(server) {
36151
36796
  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."),
36152
36797
  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).`),
36153
36798
  attachInline: attachInlineParam,
36154
- account: accountParam
36799
+ account: accountParam,
36800
+ confirmToken: confirmTokenParam
36155
36801
  })
36156
- }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account }, ctx) => {
36802
+ }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account, confirmToken }, ctx) => {
36157
36803
  confinePaths(attach, "attach");
36158
36804
  const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
36159
36805
  if (cc) args.push(`--cc=${cc}`);
@@ -36165,7 +36811,7 @@ function registerGmailTools(server) {
36165
36811
  const inline = inlineAttachmentArgs("attach", attachInline, args);
36166
36812
  args.push(...inline);
36167
36813
  const recipients = extractEmails(to, cc, bcc);
36168
- const confirmation = requireGmailDispatchConfirmation(ctx, "gmail.send", {
36814
+ const confirmation = await requireGmailDispatchConfirmation(ctx, "gmail.send", {
36169
36815
  to,
36170
36816
  cc,
36171
36817
  bcc,
@@ -36178,6 +36824,32 @@ function registerGmailTools(server) {
36178
36824
  quoting: Boolean(quote),
36179
36825
  attachmentCount: (attach?.length ?? 0) + (attachInline?.length ?? 0),
36180
36826
  attachments: attachmentNames(attach, attachInline)
36827
+ }, {
36828
+ tool: "gog_gmail_send",
36829
+ account,
36830
+ confirmToken,
36831
+ // Nothing is stored between the phases: the payload IS the arguments,
36832
+ // so phase 2 must repeat them and any difference is a changed send.
36833
+ subject: () => {
36834
+ const attachments = attachmentDetails(attach, attachInline);
36835
+ const from = senderPreview(account);
36836
+ return {
36837
+ target: replyToMessageId ?? threadId ?? "",
36838
+ payload: { from, to, cc, bcc, subject, body, attachments, threadId, inReplyTo: replyToMessageId, quote: Boolean(quote) },
36839
+ preview: {
36840
+ from,
36841
+ to,
36842
+ cc,
36843
+ bcc,
36844
+ subject,
36845
+ body,
36846
+ attachments: attachmentPreview(attachments),
36847
+ threadId,
36848
+ inReplyTo: replyToMessageId,
36849
+ quotesOriginal: Boolean(quote)
36850
+ }
36851
+ };
36852
+ }
36181
36853
  });
36182
36854
  if (confirmation) return confirmation;
36183
36855
  const result = await runOrDiagnose(args, { account });
@@ -36185,22 +36857,22 @@ function registerGmailTools(server) {
36185
36857
  return result;
36186
36858
  });
36187
36859
  server.registerTool("gog_gmail_reply", {
36188
- 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.',
36860
+ 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,
36189
36861
  annotations: { destructiveHint: true },
36190
36862
  inputSchema: sendReplySchema
36191
- }, async ({ messageId, account, ...flags }, ctx) => {
36863
+ }, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
36192
36864
  assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
36193
36865
  confineReplyPaths(flags);
36194
- return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx);
36866
+ return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx, confirmToken);
36195
36867
  });
36196
36868
  server.registerTool("gog_gmail_reply_all", {
36197
- 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.',
36869
+ 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,
36198
36870
  annotations: { destructiveHint: true },
36199
36871
  inputSchema: sendReplySchema
36200
- }, async ({ messageId, account, ...flags }, ctx) => {
36872
+ }, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
36201
36873
  assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
36202
36874
  confineReplyPaths(flags);
36203
- return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx);
36875
+ return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx, confirmToken);
36204
36876
  });
36205
36877
  registerRunTool(server, {
36206
36878
  service: "gmail",
@@ -36541,7 +37213,7 @@ function registerTasksTools(server) {
36541
37213
  }
36542
37214
 
36543
37215
  // src/server.ts
36544
- var VERSION = true ? "4.3.0" : "0.0.0";
37216
+ var VERSION = true ? "4.5.0" : "0.0.0";
36545
37217
  var BASE_TOOL_REGISTRARS = [
36546
37218
  registerApiTools,
36547
37219
  registerAppScriptTools,
@@ -36593,12 +37265,12 @@ async function bootstrapGogAuth(env = process.env, options = {}) {
36593
37265
  const [clientId, clientSecret, refreshToken, account] = values;
36594
37266
  const { spawner } = options;
36595
37267
  try {
36596
- const { createHash: createHash2 } = await import("node:crypto");
37268
+ const { createHash: createHash5 } = await import("node:crypto");
36597
37269
  const { mkdir, readFile, writeFile, chmod } = await import("node:fs/promises");
36598
37270
  const home = options.home ?? (await import("node:os")).homedir();
36599
37271
  const dir = join5(home, ".gogcli-mcp");
36600
37272
  const marker = join5(dir, AUTH_BOOTSTRAP_MARKER);
36601
- const fingerprint = createHash2("sha256").update(values.join("\0")).digest("hex");
37273
+ const fingerprint = createHash5("sha256").update(values.join("\0")).digest("hex");
36602
37274
  const previous = await readFile(marker, "utf8").catch(() => void 0);
36603
37275
  if (previous === fingerprint && await accountListed(account, spawner)) return "present";
36604
37276
  await run(
@@ -36623,7 +37295,7 @@ async function bootstrapGogAuth(env = process.env, options = {}) {
36623
37295
  }
36624
37296
 
36625
37297
  // src/blob-urls.ts
36626
- import { createHmac as createHmac2 } from "node:crypto";
37298
+ import { createHmac as createHmac3 } from "node:crypto";
36627
37299
  var BLOB_URL_MAX_TTL_MS = 24 * 60 * 60 * 1e3;
36628
37300
  var BLOB_URL_CEILING_MARGIN_MS = 60 * 1e3;
36629
37301
  var BLOB_URL_DEFAULT_TTL_MS = 60 * 60 * 1e3;
@@ -36635,8 +37307,8 @@ ${exp}`;
36635
37307
  function writePayload(objectKey, contentType, exp) {
36636
37308
  return `put\0${objectKey}\0${contentType}\0${exp}`;
36637
37309
  }
36638
- function sign(signingKey, payload) {
36639
- return createHmac2("sha256", signingKey).update(payload, "utf8").digest("base64url");
37310
+ function sign2(signingKey, payload) {
37311
+ return createHmac3("sha256", signingKey).update(payload, "utf8").digest("base64url");
36640
37312
  }
36641
37313
  function objectPathSegments(rest) {
36642
37314
  const segments = rest.split("/");
@@ -36672,7 +37344,7 @@ function createBlobUrlMinter(config2) {
36672
37344
  function mint(rest, exp, payload) {
36673
37345
  const segments = objectPathSegments(rest);
36674
37346
  const url2 = `${basePrefix}/${segments.map(encodeURIComponent).join("/")}`;
36675
- const signature = sign(config2.signingKey, payload(`${registrationId}/${segments.join("/")}`));
37347
+ const signature = sign2(config2.signingKey, payload(`${registrationId}/${segments.join("/")}`));
36676
37348
  return `${url2}?exp=${exp}&sig=${signature}`;
36677
37349
  }
36678
37350
  return {
@@ -36821,6 +37493,7 @@ export {
36821
37493
  BLOB_UPLOAD_TIMEOUT_MS,
36822
37494
  BLOB_URL_DEFAULT_TTL_MS,
36823
37495
  BLOB_URL_MAX_TTL_MS,
37496
+ CONFIRM_FALLBACK_DESCRIPTION,
36824
37497
  FILE_ROOTS_ENV,
36825
37498
  INLINE_ATTACHMENT_LIMITS_TEXT,
36826
37499
  MAX_BLOB_UPLOAD_BYTES,
@@ -36835,7 +37508,9 @@ export {
36835
37508
  appendReplyFlags,
36836
37509
  assertNotBoth,
36837
37510
  attachInlineParam,
37511
+ attachmentDetails,
36838
37512
  attachmentNames,
37513
+ attachmentPreview,
36839
37514
  authToolsFor,
36840
37515
  blobStoreFromEnv,
36841
37516
  bodyPreview,
@@ -36843,6 +37518,7 @@ export {
36843
37518
  confineAtFile,
36844
37519
  confinePath,
36845
37520
  confinePaths,
37521
+ confirmTokenParam,
36846
37522
  createBlobUrlMinter,
36847
37523
  defaultFileRoot,
36848
37524
  diagnose,
@@ -36865,6 +37541,7 @@ export {
36865
37541
  pos,
36866
37542
  prepareDownloadRoot,
36867
37543
  pushPaginationFlags,
37544
+ readCourse,
36868
37545
  registerApiTools,
36869
37546
  registerAppScriptTools,
36870
37547
  registerAuthTools,
@@ -36881,12 +37558,14 @@ export {
36881
37558
  registerTasksTools,
36882
37559
  removeDownload,
36883
37560
  replySchema,
37561
+ requireDispatchConfirmation,
36884
37562
  requireGmailDispatchConfirmation,
36885
37563
  resolvePageToken,
36886
37564
  resultText,
36887
37565
  run,
36888
37566
  runBinary,
36889
37567
  runOrDiagnose,
37568
+ senderPreview,
36890
37569
  stripConsumedPageToken,
36891
37570
  uploadToBlobStore
36892
37571
  };