gogcli-mcp 4.3.0 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +647 -67
- package/dist/lib.js +654 -67
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/dispatch-confirmation.ts +277 -0
- package/src/gmail-dispatch-guard.ts +36 -36
- package/src/lib.ts +9 -0
- package/src/send-confirm-token.ts +167 -0
- package/src/tools/api.ts +24 -3
- package/src/tools/calendar.ts +168 -7
- package/src/tools/chat.ts +78 -5
- package/src/tools/classroom.ts +83 -3
- package/src/tools/drive.ts +55 -3
- package/src/tools/gmail.ts +111 -20
- package/tests/gmail-dispatch-guard.test.ts +227 -14
- package/tests/send-confirm-token.test.ts +200 -0
- package/tests/tools/calendar.test.ts +4 -1
- package/tests/tools/chat.test.ts +4 -1
- package/tests/tools/classroom.test.ts +4 -1
- package/tests/tools/dispatch-gates.test.ts +396 -0
- package/tests/tools/drive.test.ts +4 -1
- package/tests/tools/gmail-confirm-token.test.ts +274 -0
- package/tests/tools/run-vets.test.ts +131 -0
package/dist/lib.js
CHANGED
|
@@ -33289,9 +33289,9 @@ function offsetAt(instant, tz) {
|
|
|
33289
33289
|
const w = wallPartsIn(instant, tz);
|
|
33290
33290
|
const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
|
|
33291
33291
|
const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
|
|
33292
|
-
const
|
|
33292
|
+
const sign3 = minutes < 0 ? "-" : "+";
|
|
33293
33293
|
const abs = Math.abs(minutes);
|
|
33294
|
-
return `${
|
|
33294
|
+
return `${sign3}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
33295
33295
|
}
|
|
33296
33296
|
function wallPartsIn(instant, tz) {
|
|
33297
33297
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
@@ -34199,13 +34199,239 @@ function assertNotBoth(inlineParam, fileParam, inlineValue, fileValue) {
|
|
|
34199
34199
|
}
|
|
34200
34200
|
}
|
|
34201
34201
|
|
|
34202
|
+
// src/dispatch-confirmation.ts
|
|
34203
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
34204
|
+
import { readFileSync } from "node:fs";
|
|
34205
|
+
|
|
34206
|
+
// src/send-confirm-token.ts
|
|
34207
|
+
import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
34208
|
+
var CONFIRM_TOKEN_TTL_DEFAULT_SECONDS = 600;
|
|
34209
|
+
var PREFIX = "gct1";
|
|
34210
|
+
var secret;
|
|
34211
|
+
var spent = /* @__PURE__ */ new Map();
|
|
34212
|
+
function sendConfirmFallbackEnabled() {
|
|
34213
|
+
return readEnvVar("GOG_SEND_CONFIRM_FALLBACK")?.trim().toLowerCase() === "token";
|
|
34214
|
+
}
|
|
34215
|
+
function confirmTokenTtlSeconds() {
|
|
34216
|
+
const raw = readEnvVar("GOG_CONFIRM_TTL_SECONDS")?.trim();
|
|
34217
|
+
if (raw && /^\d+$/.test(raw)) {
|
|
34218
|
+
const n = Number(raw);
|
|
34219
|
+
if (n > 0) return n;
|
|
34220
|
+
}
|
|
34221
|
+
return CONFIRM_TOKEN_TTL_DEFAULT_SECONDS;
|
|
34222
|
+
}
|
|
34223
|
+
function hmacKey() {
|
|
34224
|
+
if (!secret) {
|
|
34225
|
+
const configured = readEnvVar("GOG_CONFIRM_SECRET");
|
|
34226
|
+
secret = configured ? Buffer.from(configured, "utf8") : randomBytes(32);
|
|
34227
|
+
}
|
|
34228
|
+
return secret;
|
|
34229
|
+
}
|
|
34230
|
+
function canonicalize(value) {
|
|
34231
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
34232
|
+
if (value && typeof value === "object") {
|
|
34233
|
+
const out = {};
|
|
34234
|
+
for (const key of Object.keys(value).sort()) {
|
|
34235
|
+
const v = value[key];
|
|
34236
|
+
if (v !== void 0) out[key] = canonicalize(v);
|
|
34237
|
+
}
|
|
34238
|
+
return out;
|
|
34239
|
+
}
|
|
34240
|
+
return value;
|
|
34241
|
+
}
|
|
34242
|
+
function hashSendPayload(payload) {
|
|
34243
|
+
return createHash2("sha256").update(JSON.stringify(canonicalize(payload))).digest("hex");
|
|
34244
|
+
}
|
|
34245
|
+
function sign(body) {
|
|
34246
|
+
return createHmac2("sha256", hmacKey()).update(`${PREFIX}.${body}`).digest("base64url");
|
|
34247
|
+
}
|
|
34248
|
+
function issueConfirmToken(binding, now = Date.now()) {
|
|
34249
|
+
const exp = now + confirmTokenTtlSeconds() * 1e3;
|
|
34250
|
+
const claims = {
|
|
34251
|
+
t: binding.tool,
|
|
34252
|
+
a: binding.account,
|
|
34253
|
+
g: binding.target,
|
|
34254
|
+
...binding.revision === void 0 ? {} : { r: binding.revision },
|
|
34255
|
+
h: binding.payloadHash,
|
|
34256
|
+
iat: now,
|
|
34257
|
+
exp,
|
|
34258
|
+
n: randomBytes(16).toString("base64url")
|
|
34259
|
+
};
|
|
34260
|
+
const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
|
|
34261
|
+
return { token: `${PREFIX}.${body}.${sign(body)}`, expiresAt: new Date(exp).toISOString() };
|
|
34262
|
+
}
|
|
34263
|
+
function signatureMatches(body, sig) {
|
|
34264
|
+
const expected = Buffer.from(sign(body), "utf8");
|
|
34265
|
+
const actual = Buffer.from(sig, "utf8");
|
|
34266
|
+
return expected.length === actual.length && timingSafeEqual2(expected, actual);
|
|
34267
|
+
}
|
|
34268
|
+
function parseClaims(body) {
|
|
34269
|
+
try {
|
|
34270
|
+
return JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
34271
|
+
} catch {
|
|
34272
|
+
return void 0;
|
|
34273
|
+
}
|
|
34274
|
+
}
|
|
34275
|
+
function verifyConfirmToken(token, binding, now = Date.now()) {
|
|
34276
|
+
for (const [nonce, exp] of spent) if (exp < now) spent.delete(nonce);
|
|
34277
|
+
const parts = token.split(".");
|
|
34278
|
+
if (parts.length !== 3 || parts[0] !== PREFIX || !parts[1] || !parts[2]) return { ok: false, error: "TOKEN_INVALID" };
|
|
34279
|
+
const [, body, sig] = parts;
|
|
34280
|
+
if (!signatureMatches(body, sig)) return { ok: false, error: "TOKEN_INVALID" };
|
|
34281
|
+
const claims = parseClaims(body);
|
|
34282
|
+
if (!claims) return { ok: false, error: "TOKEN_INVALID" };
|
|
34283
|
+
if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
|
|
34284
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
34285
|
+
}
|
|
34286
|
+
if (spent.has(claims.n)) return { ok: false, error: "TOKEN_REUSED" };
|
|
34287
|
+
if (now > claims.exp) return { ok: false, error: "TOKEN_EXPIRED" };
|
|
34288
|
+
if (claims.r !== binding.revision) return { ok: false, error: "DRAFT_CHANGED", reason: "message-id-rotated" };
|
|
34289
|
+
if (claims.h !== binding.payloadHash) return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
|
|
34290
|
+
spent.set(claims.n, claims.exp);
|
|
34291
|
+
return { ok: true };
|
|
34292
|
+
}
|
|
34293
|
+
|
|
34294
|
+
// src/dispatch-confirmation.ts
|
|
34295
|
+
var BODY_PREVIEW_MAX = 2048;
|
|
34296
|
+
function bodyPreview(text) {
|
|
34297
|
+
if (!text) return void 0;
|
|
34298
|
+
if (text.length <= BODY_PREVIEW_MAX) return text;
|
|
34299
|
+
return `${text.slice(0, BODY_PREVIEW_MAX)}\u2026 [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
|
|
34300
|
+
}
|
|
34301
|
+
function attachmentNames(paths, inline) {
|
|
34302
|
+
return [...paths ?? [], ...(inline ?? []).map((a) => a.filename)];
|
|
34303
|
+
}
|
|
34304
|
+
function attachmentDetails(paths, inline) {
|
|
34305
|
+
const out = [];
|
|
34306
|
+
for (const path of paths ?? []) {
|
|
34307
|
+
let bytes;
|
|
34308
|
+
try {
|
|
34309
|
+
bytes = readFileSync(path);
|
|
34310
|
+
} catch {
|
|
34311
|
+
bytes = void 0;
|
|
34312
|
+
}
|
|
34313
|
+
out.push(bytes ? { name: path, size: bytes.length, sha256: createHash3("sha256").update(bytes).digest("hex") } : { name: path, size: null });
|
|
34314
|
+
}
|
|
34315
|
+
for (const a of inline ?? []) {
|
|
34316
|
+
out.push({
|
|
34317
|
+
name: a.filename,
|
|
34318
|
+
size: Buffer.from(a.contentBase64, "base64").length,
|
|
34319
|
+
sha256: createHash3("sha256").update(a.contentBase64).digest("hex")
|
|
34320
|
+
});
|
|
34321
|
+
}
|
|
34322
|
+
return out;
|
|
34323
|
+
}
|
|
34324
|
+
function senderPreview(account, from) {
|
|
34325
|
+
return from ?? account ?? readEnvVar("GOG_ACCOUNT") ?? "the gog account's default address";
|
|
34326
|
+
}
|
|
34327
|
+
function attachmentPreview(details) {
|
|
34328
|
+
return details.map(({ name, size }) => ({ name, size }));
|
|
34329
|
+
}
|
|
34330
|
+
var CONFIRM_SEND_INSTRUCTION = "Show this preview to the user verbatim and send only after they explicitly approve in chat. Then call again with confirmToken.";
|
|
34331
|
+
var CONFIRM_ACTION_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
|
|
34332
|
+
var FALLBACK_HINT = "Or set GOG_SEND_CONFIRM_FALLBACK=token to enable two-step confirmation.";
|
|
34333
|
+
var CONFIRM_FALLBACK_DESCRIPTION = ' If the client cannot show that prompt (no MCP elicitation, e.g. claude.ai) and the server sets GOG_SEND_CONFIRM_FALLBACK=token, a two-step flow applies instead: call WITHOUT confirmToken and nothing is sent or changed \u2014 the result has status "confirmation-required", the full preview and a confirmToken. Show that preview to the user verbatim; only after they explicitly approve it in chat, call again with the SAME arguments plus confirmToken. The tool re-reads what it would act on and refuses (DRAFT_CHANGED, with a fresh preview and token) if it changed; TOKEN_EXPIRED / TOKEN_REUSED / TOKEN_INVALID also do nothing.';
|
|
34334
|
+
var confirmTokenParam = external_exports.string().optional().describe(
|
|
34335
|
+
`ONLY for the two-step fallback (client without MCP elicitation, server with GOG_SEND_CONFIRM_FALLBACK=token). The confirmToken from this same tool's phase-1 "confirmation-required" response, passed back ONLY after the user has seen that preview and explicitly approved it in chat \u2014 never on the first call, never invented, never reused. Call again with the same arguments. Ignored when the client supports elicitation.`
|
|
34336
|
+
);
|
|
34337
|
+
var TOKEN_ERROR_NOTE = {
|
|
34338
|
+
TOKEN_EXPIRED: "Nothing was sent or changed: the confirmToken expired. Call again WITHOUT confirmToken for a fresh preview, and ask the user to approve it again.",
|
|
34339
|
+
TOKEN_REUSED: "Nothing was sent or changed by this call: this confirmToken was already used, and one approval acts once. If doing it again is really intended, call again WITHOUT confirmToken and get a new approval.",
|
|
34340
|
+
TOKEN_INVALID: "Nothing was sent or changed: this confirmToken was not issued by this server for this tool, account and target (or the server has restarted since). Call again WITHOUT confirmToken for a fresh preview and approval."
|
|
34341
|
+
};
|
|
34342
|
+
var DRAFT_CHANGED_NOTE = {
|
|
34343
|
+
"message-id-rotated": "Nothing was sent or changed: the target was edited since the user approved it (a draft's messageId or an event's version rotated), so what would happen is not what they saw.",
|
|
34344
|
+
"payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
|
|
34345
|
+
};
|
|
34346
|
+
function isToolResult(value) {
|
|
34347
|
+
return Array.isArray(value.content);
|
|
34348
|
+
}
|
|
34349
|
+
function rejection(data) {
|
|
34350
|
+
return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
|
|
34351
|
+
}
|
|
34352
|
+
async function tokenConfirmation(op, fallback) {
|
|
34353
|
+
const subject = await fallback.subject();
|
|
34354
|
+
if (isToolResult(subject)) return subject;
|
|
34355
|
+
const binding = {
|
|
34356
|
+
tool: fallback.tool,
|
|
34357
|
+
account: fallback.account ?? readEnvVar("GOG_ACCOUNT") ?? "",
|
|
34358
|
+
target: subject.target,
|
|
34359
|
+
...subject.revision === void 0 ? {} : { revision: subject.revision },
|
|
34360
|
+
payloadHash: hashSendPayload(subject.payload)
|
|
34361
|
+
};
|
|
34362
|
+
const phaseOne = () => {
|
|
34363
|
+
const { token, expiresAt } = issueConfirmToken(binding);
|
|
34364
|
+
return {
|
|
34365
|
+
action: op,
|
|
34366
|
+
preview: subject.preview,
|
|
34367
|
+
confirmToken: token,
|
|
34368
|
+
expiresAt,
|
|
34369
|
+
ttlSeconds: confirmTokenTtlSeconds(),
|
|
34370
|
+
instruction: fallback.instruction ?? CONFIRM_ACTION_INSTRUCTION
|
|
34371
|
+
};
|
|
34372
|
+
};
|
|
34373
|
+
if (!fallback.confirmToken) {
|
|
34374
|
+
return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
|
|
34375
|
+
}
|
|
34376
|
+
const verdict = verifyConfirmToken(fallback.confirmToken, binding);
|
|
34377
|
+
if (verdict.ok) return void 0;
|
|
34378
|
+
if (verdict.error === "DRAFT_CHANGED") {
|
|
34379
|
+
return rejection({
|
|
34380
|
+
error: "DRAFT_CHANGED",
|
|
34381
|
+
reason: verdict.reason,
|
|
34382
|
+
note: `${DRAFT_CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
|
|
34383
|
+
...phaseOne()
|
|
34384
|
+
});
|
|
34385
|
+
}
|
|
34386
|
+
return rejection({ error: verdict.error, action: op, note: TOKEN_ERROR_NOTE[verdict.error] });
|
|
34387
|
+
}
|
|
34388
|
+
function gatedElsewhere(what, via, does, tool) {
|
|
34389
|
+
return `${what} ${does} and is not available through ${via}. Use ${tool}, which asks the user to confirm.`;
|
|
34390
|
+
}
|
|
34391
|
+
function hasCommandWord(args, words) {
|
|
34392
|
+
return args.find((a) => words.has(a.toLowerCase()));
|
|
34393
|
+
}
|
|
34394
|
+
async function requireDispatchConfirmation(ctx, options) {
|
|
34395
|
+
const { action, fallback } = options;
|
|
34396
|
+
if (fallback && callerAcceptsFormElicitation(ctx) === false && sendConfirmFallbackEnabled()) {
|
|
34397
|
+
return tokenConfirmation(action, fallback);
|
|
34398
|
+
}
|
|
34399
|
+
const note = fallback ? [options.unsupportedNote, FALLBACK_HINT].filter(Boolean).join(" ") : options.unsupportedNote;
|
|
34400
|
+
return requireConfirmation(ctx, {
|
|
34401
|
+
action,
|
|
34402
|
+
message: options.message,
|
|
34403
|
+
details: options.details,
|
|
34404
|
+
confirmationLabel: options.confirmationLabel,
|
|
34405
|
+
...note ? { unsupportedNote: note } : {}
|
|
34406
|
+
});
|
|
34407
|
+
}
|
|
34408
|
+
function resultText(result) {
|
|
34409
|
+
const first = result.content[0];
|
|
34410
|
+
return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
|
|
34411
|
+
}
|
|
34412
|
+
|
|
34202
34413
|
// src/tools/api.ts
|
|
34203
34414
|
var GMAIL_API_BLOCKED = /(?:\.send$|forwarding|filters\.create|filters\.update|delegates\.create)/i;
|
|
34415
|
+
var DISPATCH_API_BLOCKED = {
|
|
34416
|
+
chat: [{ method: /(?:^|\.)spaces\.messages\.create$/i, does: "posts a Chat message", tool: "gog_chat_messages_send / gog_chat_dm_send" }],
|
|
34417
|
+
drive: [{ method: /(?:^|\.)permissions\.(?:create|update)$/i, does: "grants access to a file", tool: "gog_drive_share" }],
|
|
34418
|
+
classroom: [
|
|
34419
|
+
{ method: /(?:^|\.)courses\.announcements\.create$/i, does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
34420
|
+
{ method: /(?:^|\.)invitations\.create$/i, does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
|
|
34421
|
+
],
|
|
34422
|
+
calendar: [
|
|
34423
|
+
{ method: /(?:^|\.)events\.(?:insert|import|quickadd)$/i, does: "can put an event on guests' calendars", tool: "gog_calendar_create" },
|
|
34424
|
+
{ method: /(?:^|\.)events\.(?:update|patch)$/i, does: "can change what guests see", tool: "gog_calendar_update / gog_calendar_respond" }
|
|
34425
|
+
]
|
|
34426
|
+
};
|
|
34204
34427
|
function refusedApiCall(api, method) {
|
|
34205
|
-
|
|
34428
|
+
const name = api.trim().toLowerCase();
|
|
34429
|
+
const m = method.trim();
|
|
34430
|
+
if (name === "gmail" && GMAIL_API_BLOCKED.test(m)) {
|
|
34206
34431
|
return `gmail ${method} is not available through gog_api_call: it sends or forwards mail. Use gog_gmail_send / gog_gmail_drafts_send (which ask the user to confirm) or the dedicated gog_gmail_* tool.`;
|
|
34207
34432
|
}
|
|
34208
|
-
|
|
34433
|
+
const hit = (Object.hasOwn(DISPATCH_API_BLOCKED, name) ? DISPATCH_API_BLOCKED[name] : []).find((b) => b.method.test(m));
|
|
34434
|
+
return hit ? gatedElsewhere(`${name} ${m}`, "gog_api_call", hit.does, hit.tool) : void 0;
|
|
34209
34435
|
}
|
|
34210
34436
|
function registerApiTools(server) {
|
|
34211
34437
|
server.registerTool("gog_api_list", {
|
|
@@ -34235,7 +34461,7 @@ function registerApiTools(server) {
|
|
|
34235
34461
|
return runOrDiagnose(args, { account });
|
|
34236
34462
|
});
|
|
34237
34463
|
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.",
|
|
34464
|
+
description: "Call any Discovery-described Google API method directly \u2014 an escape hatch for endpoints gog has no dedicated tool for. Find the exact api/version/method/params with gog_api_describe first. Read methods (GET/LIST) run as-is. Mutating methods (POST/PUT/PATCH/DELETE) are refused unless you set allowWrite=true \u2014 keep it false to preview, or set dryRun=true to print the intended request without sending it. Gmail send and forwarding methods (users.messages.send, users.drafts.send, forwarding/auto-forwarding, filters, delegates) are refused \u2014 use the dedicated gog_gmail_* tools, which ask the user to confirm. So are the other methods a dedicated tool asks about: Chat spaces.messages.create, Drive permissions.create/update, Classroom courses.announcements.create and invitations.create, and Calendar events.insert/import/quickAdd/update/patch.",
|
|
34239
34465
|
annotations: { destructiveHint: true },
|
|
34240
34466
|
inputSchema: external_exports.object({
|
|
34241
34467
|
api: external_exports.string().describe("Discovery API name (e.g. drive, gmail, calendar)"),
|
|
@@ -34521,6 +34747,54 @@ function authToolsFor(defaultServices) {
|
|
|
34521
34747
|
}
|
|
34522
34748
|
|
|
34523
34749
|
// src/tools/calendar.ts
|
|
34750
|
+
var CALENDAR_GATED = {
|
|
34751
|
+
create: "gog_calendar_create",
|
|
34752
|
+
add: "gog_calendar_create",
|
|
34753
|
+
new: "gog_calendar_create",
|
|
34754
|
+
update: "gog_calendar_update",
|
|
34755
|
+
edit: "gog_calendar_update",
|
|
34756
|
+
set: "gog_calendar_update",
|
|
34757
|
+
respond: "gog_calendar_respond",
|
|
34758
|
+
rsvp: "gog_calendar_respond",
|
|
34759
|
+
reply: "gog_calendar_respond"
|
|
34760
|
+
};
|
|
34761
|
+
function vetCalendarRun(subcommand, _args) {
|
|
34762
|
+
const sub = subcommand.toLowerCase();
|
|
34763
|
+
const tool = Object.hasOwn(CALENDAR_GATED, sub) ? CALENDAR_GATED[sub] : void 0;
|
|
34764
|
+
return tool ? gatedElsewhere(`gog calendar ${sub}`, "gog_calendar_run", "can change what guests see", tool) : void 0;
|
|
34765
|
+
}
|
|
34766
|
+
function parseAttendees(value) {
|
|
34767
|
+
if (!value) return [];
|
|
34768
|
+
return value.split(",").map((entry) => entry.split(";")).filter((parts) => !parts.slice(1).some((m) => m.trim().toLowerCase() === "resource")).map((parts) => parts[0].trim()).filter(Boolean);
|
|
34769
|
+
}
|
|
34770
|
+
function timeOf(t) {
|
|
34771
|
+
const v = t?.dateTime ?? t?.date;
|
|
34772
|
+
return typeof v === "string" ? v : void 0;
|
|
34773
|
+
}
|
|
34774
|
+
function eventSnapshot(raw) {
|
|
34775
|
+
let event;
|
|
34776
|
+
try {
|
|
34777
|
+
event = JSON.parse(raw)?.event;
|
|
34778
|
+
} catch {
|
|
34779
|
+
event = void 0;
|
|
34780
|
+
}
|
|
34781
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
34782
|
+
return {
|
|
34783
|
+
summary: str(event?.summary),
|
|
34784
|
+
start: timeOf(event?.start),
|
|
34785
|
+
end: timeOf(event?.end),
|
|
34786
|
+
organizer: str(event?.organizer?.email),
|
|
34787
|
+
// Guests are the OTHER people: not this account's own attendee row, not rooms.
|
|
34788
|
+
guests: (Array.isArray(event?.attendees) ? event.attendees : []).filter((a) => a.self !== true && a.resource !== true && typeof a.email === "string").map((a) => a.email),
|
|
34789
|
+
etag: str(event?.etag)
|
|
34790
|
+
};
|
|
34791
|
+
}
|
|
34792
|
+
async function readEvent(calendarId, eventId, account) {
|
|
34793
|
+
const got = await runOrDiagnose(["calendar", "event", pos(calendarId), pos(eventId)], { account });
|
|
34794
|
+
if (got.isError) return { error: got };
|
|
34795
|
+
return { event: eventSnapshot(resultText(got)) };
|
|
34796
|
+
}
|
|
34797
|
+
var CALENDAR_CONFIRM_LABEL = "Confirm that this change, which other people will see, should be made now.";
|
|
34524
34798
|
var reminderParams = {
|
|
34525
34799
|
reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
|
|
34526
34800
|
`Reminders as method:duration, e.g. ["popup:30m", "email:1d"]. Method is popup or email; duration accepts m/h/d (max 40320 minutes = 4 weeks). Google allows at most 5. These REPLACE the event's reminders \u2014 on update, pass an EMPTY array to drop custom reminders and go back to the calendar's defaults. Cannot be combined with noReminders.`
|
|
@@ -34608,7 +34882,7 @@ function registerCalendarTools(server) {
|
|
|
34608
34882
|
return runOrDiagnose(args, { account });
|
|
34609
34883
|
});
|
|
34610
34884
|
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).",
|
|
34885
|
+
description: "Create a calendar event. Set withZoom=true to attach a Zoom meeting (requires Zoom S2S OAuth setup via gog_zoom_auth_setup; the join URL + meeting ID + passcode are appended to the event description \u2014 Google rejects native conference card writes from non-Workspace-Marketplace OAuth clients). With attendees, the event lands on other people's calendars (no invitation email is sent), so the MCP host is asked to show the user a confirmation prompt with the guests, time and details first; a guest-free event is created without asking." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
34612
34886
|
annotations: { destructiveHint: false },
|
|
34613
34887
|
inputSchema: external_exports.object({
|
|
34614
34888
|
calendarId: external_exports.string().describe('Calendar ID (use "primary" for the default calendar)'),
|
|
@@ -34622,9 +34896,10 @@ function registerCalendarTools(server) {
|
|
|
34622
34896
|
timezone: external_exports.string().optional().describe("IANA timezone metadata applied to from/to (e.g. America/New_York). Sets both start and end timezone unless start/end timezone are overridden."),
|
|
34623
34897
|
withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)"),
|
|
34624
34898
|
...reminderParams,
|
|
34625
|
-
account: accountParam
|
|
34899
|
+
account: accountParam,
|
|
34900
|
+
confirmToken: confirmTokenParam
|
|
34626
34901
|
})
|
|
34627
|
-
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
|
|
34902
|
+
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account, confirmToken }, ctx) => {
|
|
34628
34903
|
const args = ["calendar", "create", pos(calendarId), `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
|
|
34629
34904
|
if (description) args.push(`--description=${description}`);
|
|
34630
34905
|
if (location) args.push(`--location=${location}`);
|
|
@@ -34633,10 +34908,28 @@ function registerCalendarTools(server) {
|
|
|
34633
34908
|
if (timezone) args.push(`--timezone=${timezone}`);
|
|
34634
34909
|
if (withZoom) args.push("--with-zoom");
|
|
34635
34910
|
pushReminderFlags(args, { reminders, noReminders });
|
|
34911
|
+
const guests = parseAttendees(attendees);
|
|
34912
|
+
if (guests.length > 0) {
|
|
34913
|
+
const event = { calendarId, summary, from, to, allDay: Boolean(allDay), timezone, location, description, guests, attendees, withZoom: Boolean(withZoom) };
|
|
34914
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
34915
|
+
action: "calendar.create",
|
|
34916
|
+
message: "Review and confirm this event, which will appear on the guests' calendars:",
|
|
34917
|
+
confirmationLabel: CALENDAR_CONFIRM_LABEL,
|
|
34918
|
+
details: event,
|
|
34919
|
+
unsupportedNote: "Create it without attendees instead; the user can add the guests in Google Calendar.",
|
|
34920
|
+
fallback: {
|
|
34921
|
+
tool: "gog_calendar_create",
|
|
34922
|
+
account,
|
|
34923
|
+
confirmToken,
|
|
34924
|
+
subject: () => ({ target: calendarId, payload: event, preview: event })
|
|
34925
|
+
}
|
|
34926
|
+
});
|
|
34927
|
+
if (confirmation) return confirmation;
|
|
34928
|
+
}
|
|
34636
34929
|
return runOrDiagnose(args, { account });
|
|
34637
34930
|
});
|
|
34638
34931
|
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.",
|
|
34932
|
+
description: "Update an existing calendar event. Zoom: withZoom adds a Zoom meeting, regenerateZoom replaces the existing one, removeZoom strips it. removeMeet clears the event's Google Meet conference data (e.g. before attaching another provider). Conference flags are independent \u2014 use one per call. A change guests can see (time, title, place, description, attendees, attachments, conferencing) on an event that has guests \u2014 or gains them \u2014 reads the event and asks the MCP host to show the user a confirmation prompt with the event as it stands and the change; no invitation email is sent. Reminder-only changes and guest-free events are updated without asking." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
34640
34933
|
annotations: { destructiveHint: false },
|
|
34641
34934
|
inputSchema: external_exports.object({
|
|
34642
34935
|
calendarId: external_exports.string().describe("Calendar ID"),
|
|
@@ -34654,9 +34947,10 @@ function registerCalendarTools(server) {
|
|
|
34654
34947
|
removeZoom: external_exports.boolean().optional().describe("Remove the event's Zoom video conference"),
|
|
34655
34948
|
removeMeet: external_exports.boolean().optional().describe("Remove the event's Google Meet video conference (clears conference data only)"),
|
|
34656
34949
|
...reminderParams,
|
|
34657
|
-
account: accountParam
|
|
34950
|
+
account: accountParam,
|
|
34951
|
+
confirmToken: confirmTokenParam
|
|
34658
34952
|
})
|
|
34659
|
-
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
|
|
34953
|
+
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account, confirmToken }, ctx) => {
|
|
34660
34954
|
const args = ["calendar", "update", pos(calendarId), pos(eventId)];
|
|
34661
34955
|
if (summary !== void 0) args.push(`--summary=${summary}`);
|
|
34662
34956
|
if (from !== void 0) args.push(`--from=${from}`);
|
|
@@ -34671,6 +34965,45 @@ function registerCalendarTools(server) {
|
|
|
34671
34965
|
if (removeZoom) args.push("--remove-zoom");
|
|
34672
34966
|
if (removeMeet) args.push("--remove-meet");
|
|
34673
34967
|
pushReminderFlags(args, { reminders, noReminders });
|
|
34968
|
+
const changes = Object.fromEntries(Object.entries({
|
|
34969
|
+
summary,
|
|
34970
|
+
from,
|
|
34971
|
+
to,
|
|
34972
|
+
description,
|
|
34973
|
+
location,
|
|
34974
|
+
attendees,
|
|
34975
|
+
addAttendees,
|
|
34976
|
+
attachments,
|
|
34977
|
+
withZoom,
|
|
34978
|
+
regenerateZoom,
|
|
34979
|
+
removeZoom,
|
|
34980
|
+
removeMeet
|
|
34981
|
+
}).filter(([, v]) => v !== void 0 && v !== false));
|
|
34982
|
+
if (Object.keys(changes).length > 0) {
|
|
34983
|
+
const read = await readEvent(calendarId, eventId, account);
|
|
34984
|
+
if (read.error) return read.error;
|
|
34985
|
+
const { etag, ...current } = read.event;
|
|
34986
|
+
const added = [...parseAttendees(attendees), ...parseAttendees(addAttendees)];
|
|
34987
|
+
if (current.guests.length > 0 || added.length > 0) {
|
|
34988
|
+
const view = { calendarId, eventId, current, changes };
|
|
34989
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
34990
|
+
action: "calendar.update",
|
|
34991
|
+
message: "Review and confirm this change to an event other people are on:",
|
|
34992
|
+
confirmationLabel: CALENDAR_CONFIRM_LABEL,
|
|
34993
|
+
details: view,
|
|
34994
|
+
unsupportedNote: "Ask the user to make this change in Google Calendar.",
|
|
34995
|
+
fallback: {
|
|
34996
|
+
tool: "gog_calendar_update",
|
|
34997
|
+
account,
|
|
34998
|
+
confirmToken,
|
|
34999
|
+
// The etag rotates on any edit, so an event changed elsewhere between
|
|
35000
|
+
// the two phases is DRAFT_CHANGED rather than edited over.
|
|
35001
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
|
|
35002
|
+
}
|
|
35003
|
+
});
|
|
35004
|
+
if (confirmation) return confirmation;
|
|
35005
|
+
}
|
|
35006
|
+
}
|
|
34674
35007
|
return runOrDiagnose(args, { account });
|
|
34675
35008
|
});
|
|
34676
35009
|
server.registerTool("gog_calendar_delete", {
|
|
@@ -34685,21 +35018,40 @@ function registerCalendarTools(server) {
|
|
|
34685
35018
|
return runOrDiagnose(["calendar", "delete", pos(calendarId), pos(eventId), "--force"], { account });
|
|
34686
35019
|
});
|
|
34687
35020
|
server.registerTool("gog_calendar_respond", {
|
|
34688
|
-
description: "Respond to a calendar event invitation.",
|
|
35021
|
+
description: "Respond to a calendar event invitation. The organizer sees the response, so this reads the event and asks the MCP host to show the user a confirmation prompt with the event, organizer, response and comment; nothing is recorded unless they accept." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
34689
35022
|
annotations: { destructiveHint: true },
|
|
34690
35023
|
inputSchema: external_exports.object({
|
|
34691
35024
|
calendarId: external_exports.string().describe("Calendar ID"),
|
|
34692
35025
|
eventId: external_exports.string().describe("Event ID"),
|
|
34693
35026
|
status: external_exports.enum(["accepted", "declined", "tentative"]).describe("Response status"),
|
|
34694
35027
|
comment: external_exports.string().optional().describe("Optional comment to include with response"),
|
|
34695
|
-
account: accountParam
|
|
35028
|
+
account: accountParam,
|
|
35029
|
+
confirmToken: confirmTokenParam
|
|
34696
35030
|
})
|
|
34697
|
-
}, async ({ calendarId, eventId, status, comment, account }) => {
|
|
35031
|
+
}, async ({ calendarId, eventId, status, comment, account, confirmToken }, ctx) => {
|
|
34698
35032
|
const args = ["calendar", "respond", pos(calendarId), pos(eventId), `--status=${status}`];
|
|
34699
35033
|
if (comment) args.push(`--comment=${comment}`);
|
|
35034
|
+
const read = await readEvent(calendarId, eventId, account);
|
|
35035
|
+
if (read.error) return read.error;
|
|
35036
|
+
const { etag, ...event } = read.event;
|
|
35037
|
+
const view = { calendarId, eventId, event, response: status, ...comment ? { comment } : {} };
|
|
35038
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
35039
|
+
action: "calendar.respond",
|
|
35040
|
+
message: "Review and confirm this response, which the organizer will see:",
|
|
35041
|
+
confirmationLabel: "Confirm that this response should be recorded now.",
|
|
35042
|
+
details: view,
|
|
35043
|
+
unsupportedNote: "Ask the user to respond from Google Calendar.",
|
|
35044
|
+
fallback: {
|
|
35045
|
+
tool: "gog_calendar_respond",
|
|
35046
|
+
account,
|
|
35047
|
+
confirmToken,
|
|
35048
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
|
|
35049
|
+
}
|
|
35050
|
+
});
|
|
35051
|
+
if (confirmation) return confirmation;
|
|
34700
35052
|
return runOrDiagnose(args, { account });
|
|
34701
35053
|
});
|
|
34702
|
-
registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"' });
|
|
35054
|
+
registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"', vet: vetCalendarRun });
|
|
34703
35055
|
}
|
|
34704
35056
|
|
|
34705
35057
|
// src/attachments.ts
|
|
@@ -34783,6 +35135,23 @@ function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
|
|
|
34783
35135
|
}
|
|
34784
35136
|
|
|
34785
35137
|
// src/tools/chat.ts
|
|
35138
|
+
var CHAT_SEND_WORDS = /* @__PURE__ */ new Set(["send", "create", "post"]);
|
|
35139
|
+
function vetChatRun(subcommand, args) {
|
|
35140
|
+
const sub = subcommand.toLowerCase();
|
|
35141
|
+
if (sub !== "messages" && sub !== "dm") return void 0;
|
|
35142
|
+
const word = hasCommandWord(args, CHAT_SEND_WORDS);
|
|
35143
|
+
if (!word) return void 0;
|
|
35144
|
+
return gatedElsewhere(
|
|
35145
|
+
`gog chat ${sub} ${word.toLowerCase()}`,
|
|
35146
|
+
"gog_chat_run",
|
|
35147
|
+
"posts a message",
|
|
35148
|
+
sub === "dm" ? "gog_chat_dm_send" : "gog_chat_messages_send"
|
|
35149
|
+
);
|
|
35150
|
+
}
|
|
35151
|
+
var CHAT_CONFIRM = {
|
|
35152
|
+
message: "Review and confirm this Google Chat message:",
|
|
35153
|
+
confirmationLabel: "Confirm that this Chat message should be posted now."
|
|
35154
|
+
};
|
|
34786
35155
|
function registerChatTools(server) {
|
|
34787
35156
|
const workspaceOnlyNote = ' WORKSPACE ONLY: Google Chat has no API for consumer accounts, so this fails on an @gmail.com account with "chat requires a Google Workspace account". That is the ACCOUNT, not the token \u2014 re-authorizing or adding scopes will not help.';
|
|
34788
35157
|
const spaceParam = external_exports.string().describe(
|
|
@@ -34888,7 +35257,7 @@ function registerChatTools(server) {
|
|
|
34888
35257
|
return runOrDiagnose(args, { account });
|
|
34889
35258
|
});
|
|
34890
35259
|
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,
|
|
35260
|
+
description: "Post a message to a Chat space. THIS IS IMMEDIATELY VISIBLE TO EVERYONE IN THE SPACE and cannot be unsent through this tool, so treat it like sending mail, not like saving a draft. Pass `thread` to reply inside an existing conversation (from gog_chat_threads_list or a message's thread field); omit it to start a new one. Text supports Chat's markdown-ish formatting (*bold*, _italic_, `code`). Asks the MCP host to show the user a confirmation prompt with the space, thread, text and attachments first; nothing is posted unless they accept." + CONFIRM_FALLBACK_DESCRIPTION + workspaceOnlyNote,
|
|
34892
35261
|
annotations: { destructiveHint: true },
|
|
34893
35262
|
inputSchema: external_exports.object({
|
|
34894
35263
|
space: spaceParam,
|
|
@@ -34898,9 +35267,10 @@ function registerChatTools(server) {
|
|
|
34898
35267
|
"Attachment file paths, read WHERE GOG RUNS. On a hosted or remote deployment that is not your machine \u2014 use attachInline there instead. Must be inside the server's GOG_FILE_ROOTS directories (default ~/gogcli-mcp-files)."
|
|
34899
35268
|
),
|
|
34900
35269
|
attachInline: attachInlineParam,
|
|
34901
|
-
account: accountParam
|
|
35270
|
+
account: accountParam,
|
|
35271
|
+
confirmToken: confirmTokenParam
|
|
34902
35272
|
})
|
|
34903
|
-
}, async ({ space, text, thread, attach, attachInline, account }) => {
|
|
35273
|
+
}, async ({ space, text, thread, attach, attachInline, account, confirmToken }, ctx) => {
|
|
34904
35274
|
confinePaths(attach, "attach");
|
|
34905
35275
|
if (text === void 0 && !attach?.length && !attachInline?.length) {
|
|
34906
35276
|
throw new Error("A Chat message needs text, an attachment, or both.");
|
|
@@ -34910,20 +35280,56 @@ function registerChatTools(server) {
|
|
|
34910
35280
|
if (thread) args.push(`--thread=${thread}`);
|
|
34911
35281
|
if (attach) for (const path of attach) args.push(`--attach=${path}`);
|
|
34912
35282
|
args.push(...inlineAttachmentArgs("attach", attachInline, args));
|
|
35283
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
35284
|
+
...CHAT_CONFIRM,
|
|
35285
|
+
action: "chat.message-send",
|
|
35286
|
+
details: { space, thread, textPreview: bodyPreview(text), attachments: attachmentNames(attach, attachInline) },
|
|
35287
|
+
fallback: {
|
|
35288
|
+
tool: "gog_chat_messages_send",
|
|
35289
|
+
account,
|
|
35290
|
+
confirmToken,
|
|
35291
|
+
subject: () => {
|
|
35292
|
+
const attachments = attachmentDetails(attach, attachInline);
|
|
35293
|
+
const from = senderPreview(account);
|
|
35294
|
+
return {
|
|
35295
|
+
target: space,
|
|
35296
|
+
payload: { from, space, thread, text, attachments },
|
|
35297
|
+
preview: { from, space, thread, text, attachments: attachmentPreview(attachments) }
|
|
35298
|
+
};
|
|
35299
|
+
}
|
|
35300
|
+
}
|
|
35301
|
+
});
|
|
35302
|
+
if (confirmation) return confirmation;
|
|
34913
35303
|
return runOrDiagnose(args, { account });
|
|
34914
35304
|
});
|
|
34915
35305
|
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,
|
|
35306
|
+
description: "Send a direct message to one person by email address, creating the DM space if this is the first message. Delivered immediately and cannot be unsent through this tool. For a room rather than a person, use gog_chat_messages_send. Asks the MCP host to show the user a confirmation prompt with the recipient and text first; nothing is sent unless they accept." + CONFIRM_FALLBACK_DESCRIPTION + workspaceOnlyNote,
|
|
34917
35307
|
annotations: { destructiveHint: true },
|
|
34918
35308
|
inputSchema: external_exports.object({
|
|
34919
35309
|
email: external_exports.string().describe("Recipient email address"),
|
|
34920
35310
|
text: external_exports.string().describe("Message text"),
|
|
34921
35311
|
thread: threadParam,
|
|
34922
|
-
account: accountParam
|
|
35312
|
+
account: accountParam,
|
|
35313
|
+
confirmToken: confirmTokenParam
|
|
34923
35314
|
})
|
|
34924
|
-
}, async ({ email: email3, text, thread, account }) => {
|
|
35315
|
+
}, async ({ email: email3, text, thread, account, confirmToken }, ctx) => {
|
|
34925
35316
|
const args = ["chat", "dm", "send", pos(email3), `--text=${text}`];
|
|
34926
35317
|
if (thread) args.push(`--thread=${thread}`);
|
|
35318
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
35319
|
+
...CHAT_CONFIRM,
|
|
35320
|
+
action: "chat.dm-send",
|
|
35321
|
+
details: { to: email3, thread, textPreview: bodyPreview(text) },
|
|
35322
|
+
fallback: {
|
|
35323
|
+
tool: "gog_chat_dm_send",
|
|
35324
|
+
account,
|
|
35325
|
+
confirmToken,
|
|
35326
|
+
subject: () => {
|
|
35327
|
+
const from = senderPreview(account);
|
|
35328
|
+
return { target: email3, payload: { from, to: email3, thread, text }, preview: { from, to: email3, thread, text } };
|
|
35329
|
+
}
|
|
35330
|
+
}
|
|
35331
|
+
});
|
|
35332
|
+
if (confirmation) return confirmation;
|
|
34927
35333
|
return runOrDiagnose(args, { account });
|
|
34928
35334
|
});
|
|
34929
35335
|
server.registerTool("gog_chat_dm_space", {
|
|
@@ -34978,11 +35384,45 @@ function registerChatTools(server) {
|
|
|
34978
35384
|
registerRunTool(server, {
|
|
34979
35385
|
service: "chat",
|
|
34980
35386
|
examples: '"spaces", "messages", "dm"',
|
|
35387
|
+
vet: vetChatRun,
|
|
34981
35388
|
note: "Google Chat has no API for consumer accounts: every chat subcommand fails on an @gmail.com account regardless of scopes."
|
|
34982
35389
|
});
|
|
34983
35390
|
}
|
|
34984
35391
|
|
|
34985
35392
|
// src/tools/classroom.ts
|
|
35393
|
+
var CLASSROOM_CREATE_WORDS = /* @__PURE__ */ new Set(["create", "add", "new"]);
|
|
35394
|
+
var CLASSROOM_GATED = {
|
|
35395
|
+
announcements: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
35396
|
+
announcement: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
35397
|
+
ann: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
35398
|
+
invitations: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
|
|
35399
|
+
invitation: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
|
|
35400
|
+
invites: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
|
|
35401
|
+
};
|
|
35402
|
+
function vetClassroomRun(subcommand, args) {
|
|
35403
|
+
const sub = subcommand.toLowerCase();
|
|
35404
|
+
const gated = Object.hasOwn(CLASSROOM_GATED, sub) ? CLASSROOM_GATED[sub] : void 0;
|
|
35405
|
+
if (!gated) return void 0;
|
|
35406
|
+
const word = hasCommandWord(args, CLASSROOM_CREATE_WORDS);
|
|
35407
|
+
return word ? gatedElsewhere(`gog classroom ${sub} ${word.toLowerCase()}`, "gog_classroom_run", gated.does, gated.tool) : void 0;
|
|
35408
|
+
}
|
|
35409
|
+
async function readCourse(courseId, account, runner = runOrDiagnose) {
|
|
35410
|
+
const got = await runner(["classroom", "courses", "get", pos(courseId)], { account });
|
|
35411
|
+
if (got.isError) return { error: got };
|
|
35412
|
+
let course;
|
|
35413
|
+
try {
|
|
35414
|
+
course = JSON.parse(resultText(got))?.course;
|
|
35415
|
+
} catch {
|
|
35416
|
+
course = void 0;
|
|
35417
|
+
}
|
|
35418
|
+
return {
|
|
35419
|
+
course: {
|
|
35420
|
+
id: courseId,
|
|
35421
|
+
...typeof course?.name === "string" ? { name: course.name } : {},
|
|
35422
|
+
...typeof course?.section === "string" ? { section: course.section } : {}
|
|
35423
|
+
}
|
|
35424
|
+
};
|
|
35425
|
+
}
|
|
34986
35426
|
function registerClassroomTools(server) {
|
|
34987
35427
|
server.registerTool("gog_classroom_courses_list", {
|
|
34988
35428
|
description: "List Google Classroom courses.",
|
|
@@ -35265,19 +35705,42 @@ function registerClassroomTools(server) {
|
|
|
35265
35705
|
return runOrDiagnose(["classroom", "announcements", "get", pos(courseId), pos(announcementId)], { account });
|
|
35266
35706
|
});
|
|
35267
35707
|
server.registerTool("gog_classroom_announcements_create", {
|
|
35268
|
-
description: "Create an announcement in a Google Classroom course.",
|
|
35708
|
+
description: "Create an announcement in a Google Classroom course. Unless state is DRAFT (which students cannot see), this reads the course and asks the MCP host to show the user a confirmation prompt with the class, the full text and when it publishes; nothing is posted unless they accept. To stage one without asking, pass state DRAFT." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
35269
35709
|
annotations: { destructiveHint: true },
|
|
35270
35710
|
inputSchema: external_exports.object({
|
|
35271
35711
|
courseId: external_exports.string().describe("Course ID"),
|
|
35272
35712
|
text: external_exports.string().describe("Announcement text"),
|
|
35273
|
-
state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State"),
|
|
35713
|
+
state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State (DRAFT is visible only to teachers and needs no confirmation)"),
|
|
35274
35714
|
scheduled: external_exports.string().optional().describe("Scheduled publish time"),
|
|
35275
|
-
account: accountParam
|
|
35715
|
+
account: accountParam,
|
|
35716
|
+
confirmToken: confirmTokenParam
|
|
35276
35717
|
})
|
|
35277
|
-
}, async ({ courseId, text, state, scheduled, account }) => {
|
|
35718
|
+
}, async ({ courseId, text, state, scheduled, account, confirmToken }, ctx) => {
|
|
35278
35719
|
const args = ["classroom", "announcements", "create", pos(courseId), `--text=${text}`];
|
|
35279
35720
|
if (state) args.push(`--state=${state}`);
|
|
35280
35721
|
if (scheduled) args.push(`--scheduled=${scheduled}`);
|
|
35722
|
+
if (state !== "DRAFT") {
|
|
35723
|
+
const read = await readCourse(courseId, account);
|
|
35724
|
+
if (read.error) return read.error;
|
|
35725
|
+
const publishes = scheduled ? `at ${scheduled}` : "immediately";
|
|
35726
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
35727
|
+
action: "classroom.announcement-create",
|
|
35728
|
+
message: "Review and confirm this Classroom announcement:",
|
|
35729
|
+
confirmationLabel: "Confirm that this announcement should be posted to the class.",
|
|
35730
|
+
details: { course: read.course, publishes, textPreview: bodyPreview(text) },
|
|
35731
|
+
unsupportedNote: "Create it with state DRAFT instead; the user can review and post it from Classroom.",
|
|
35732
|
+
fallback: {
|
|
35733
|
+
tool: "gog_classroom_announcements_create",
|
|
35734
|
+
account,
|
|
35735
|
+
confirmToken,
|
|
35736
|
+
subject: () => {
|
|
35737
|
+
const view = { course: read.course, publishes, text };
|
|
35738
|
+
return { target: courseId, payload: view, preview: view };
|
|
35739
|
+
}
|
|
35740
|
+
}
|
|
35741
|
+
});
|
|
35742
|
+
if (confirmation) return confirmation;
|
|
35743
|
+
}
|
|
35281
35744
|
return runOrDiagnose(args, { account });
|
|
35282
35745
|
});
|
|
35283
35746
|
server.registerTool("gog_classroom_topics_list", {
|
|
@@ -35367,6 +35830,7 @@ function registerClassroomTools(server) {
|
|
|
35367
35830
|
registerRunTool(server, {
|
|
35368
35831
|
service: "classroom",
|
|
35369
35832
|
examples: '"guardians", "materials", "guardian-invitations"',
|
|
35833
|
+
vet: vetClassroomRun,
|
|
35370
35834
|
note: "Covers anything not wrapped by the dedicated tools (guardians, guardian-invitations, materials, coursework assignees, announcement assignees, etc.)."
|
|
35371
35835
|
});
|
|
35372
35836
|
}
|
|
@@ -35527,7 +35991,18 @@ function registerDocsTools(server) {
|
|
|
35527
35991
|
}
|
|
35528
35992
|
|
|
35529
35993
|
// src/tools/drive.ts
|
|
35994
|
+
function vetDriveRun(subcommand, _args) {
|
|
35995
|
+
return subcommand.toLowerCase() === "share" ? gatedElsewhere("gog drive share", "gog_drive_run", "grants access to a file", "gog_drive_share") : void 0;
|
|
35996
|
+
}
|
|
35530
35997
|
var GOOGLE_DOC_MIME = "application/vnd.google-apps.document";
|
|
35998
|
+
function shareTargetMeta(raw) {
|
|
35999
|
+
try {
|
|
36000
|
+
const { name, mimeType } = fileMeta(raw);
|
|
36001
|
+
return { name, mimeType };
|
|
36002
|
+
} catch {
|
|
36003
|
+
return {};
|
|
36004
|
+
}
|
|
36005
|
+
}
|
|
35531
36006
|
function fileMeta(raw) {
|
|
35532
36007
|
const parsed = JSON.parse(raw);
|
|
35533
36008
|
const f = parsed.file ?? parsed;
|
|
@@ -35650,7 +36125,7 @@ function registerDriveTools(server) {
|
|
|
35650
36125
|
return runOrDiagnose(args, { account });
|
|
35651
36126
|
});
|
|
35652
36127
|
server.registerTool("gog_drive_share", {
|
|
35653
|
-
description: "Share a Google Drive file or folder.",
|
|
36128
|
+
description: "Share a Google Drive file or folder. Granting access is the risk even though no email is sent (gog does not notify by default), so this reads the file and asks the MCP host to show the user a confirmation prompt naming it, who gets access and with what role; nothing is shared unless they accept." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
35654
36129
|
annotations: { destructiveHint: true },
|
|
35655
36130
|
inputSchema: external_exports.object({
|
|
35656
36131
|
fileId: external_exports.string().describe("File or folder ID"),
|
|
@@ -35658,9 +36133,35 @@ function registerDriveTools(server) {
|
|
|
35658
36133
|
email: external_exports.string().optional().describe("User email (required when to=user)"),
|
|
35659
36134
|
domain: external_exports.string().optional().describe("Domain (required when to=domain)"),
|
|
35660
36135
|
role: external_exports.enum(["reader", "writer"]).optional().describe("Permission role (default: reader)"),
|
|
35661
|
-
account: accountParam
|
|
36136
|
+
account: accountParam,
|
|
36137
|
+
confirmToken: confirmTokenParam
|
|
35662
36138
|
})
|
|
35663
|
-
}, async ({ fileId, to, email: email3, domain: domain2, role, account }) => {
|
|
36139
|
+
}, async ({ fileId, to, email: email3, domain: domain2, role, account, confirmToken }, ctx) => {
|
|
36140
|
+
const got = await runOrDiagnose(["drive", "get", pos(fileId)], { account });
|
|
36141
|
+
if (got.isError) return got;
|
|
36142
|
+
const file2 = { id: fileId, ...shareTargetMeta(resultText(got)) };
|
|
36143
|
+
const grant = {
|
|
36144
|
+
file: file2,
|
|
36145
|
+
to,
|
|
36146
|
+
...email3 ? { email: email3 } : {},
|
|
36147
|
+
...domain2 ? { domain: domain2 } : {},
|
|
36148
|
+
role: role ?? "reader",
|
|
36149
|
+
publicLink: to === "anyone"
|
|
36150
|
+
};
|
|
36151
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
36152
|
+
action: "drive.share",
|
|
36153
|
+
message: to === "anyone" ? "Review and confirm making this file available to ANYONE with the link:" : "Review and confirm this Drive share:",
|
|
36154
|
+
confirmationLabel: "Confirm that this access should be granted now.",
|
|
36155
|
+
details: grant,
|
|
36156
|
+
unsupportedNote: "Ask the user to share it themselves from Google Drive.",
|
|
36157
|
+
fallback: {
|
|
36158
|
+
tool: "gog_drive_share",
|
|
36159
|
+
account,
|
|
36160
|
+
confirmToken,
|
|
36161
|
+
subject: () => ({ target: fileId, payload: grant, preview: grant })
|
|
36162
|
+
}
|
|
36163
|
+
});
|
|
36164
|
+
if (confirmation) return confirmation;
|
|
35664
36165
|
const args = ["drive", "share", pos(fileId), `--to=${to}`];
|
|
35665
36166
|
if (email3) args.push(`--email=${email3}`);
|
|
35666
36167
|
if (domain2) args.push(`--domain=${domain2}`);
|
|
@@ -35773,7 +36274,7 @@ function registerDriveTools(server) {
|
|
|
35773
36274
|
return diagnose(err);
|
|
35774
36275
|
}
|
|
35775
36276
|
});
|
|
35776
|
-
registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"' });
|
|
36277
|
+
registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"', vet: vetDriveRun });
|
|
35777
36278
|
}
|
|
35778
36279
|
|
|
35779
36280
|
// src/gmail-results.ts
|
|
@@ -35899,24 +36400,16 @@ var STAGING_TWIN = {
|
|
|
35899
36400
|
var UNSUPPORTED_NOTE2 = {
|
|
35900
36401
|
"gmail.drafts-send": "The draft is still saved: ask the user to review it and send it from Gmail."
|
|
35901
36402
|
};
|
|
35902
|
-
|
|
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) {
|
|
36403
|
+
async function requireGmailDispatchConfirmation(ctx, op, details, fallback) {
|
|
35912
36404
|
const twin = STAGING_TWIN[op];
|
|
35913
36405
|
const note = twin ? `Stage it with ${twin} instead; the user can review the draft and send it from Gmail (gog_gmail_drafts_send also asks for confirmation).` : UNSUPPORTED_NOTE2[op];
|
|
35914
|
-
return
|
|
36406
|
+
return requireDispatchConfirmation(ctx, {
|
|
35915
36407
|
action: op,
|
|
35916
36408
|
message: op === "gmail.filter-forward" ? "Review and confirm this mail-forwarding filter:" : "Review and confirm this email dispatch:",
|
|
35917
36409
|
details,
|
|
35918
36410
|
confirmationLabel: op === "gmail.filter-forward" ? "Confirm that matching mail should be forwarded automatically from now on." : "Confirm that this email should be sent now.",
|
|
35919
|
-
|
|
36411
|
+
unsupportedNote: note,
|
|
36412
|
+
...fallback ? { fallback: { instruction: CONFIRM_SEND_INSTRUCTION, ...fallback } } : {}
|
|
35920
36413
|
});
|
|
35921
36414
|
}
|
|
35922
36415
|
var EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/gi;
|
|
@@ -35970,10 +36463,6 @@ function logGmailDispatch(tool, recipients, account) {
|
|
|
35970
36463
|
process.stderr.write(`${JSON.stringify(event)}
|
|
35971
36464
|
`);
|
|
35972
36465
|
}
|
|
35973
|
-
function resultText(result) {
|
|
35974
|
-
const first = result.content[0];
|
|
35975
|
-
return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
|
|
35976
|
-
}
|
|
35977
36466
|
|
|
35978
36467
|
// src/tools/gmail.ts
|
|
35979
36468
|
var replySchema = {
|
|
@@ -36021,7 +36510,7 @@ function appendReplyFlags(args, f) {
|
|
|
36021
36510
|
if (f.signatureFile) args.push(`--signature-file=${f.signatureFile}`);
|
|
36022
36511
|
args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
|
|
36023
36512
|
}
|
|
36024
|
-
var sendReplySchema = external_exports.object(replySchema);
|
|
36513
|
+
var sendReplySchema = external_exports.object({ ...replySchema, confirmToken: confirmTokenParam });
|
|
36025
36514
|
function parseMetadataHeaders(raw) {
|
|
36026
36515
|
let parsed;
|
|
36027
36516
|
try {
|
|
@@ -36046,12 +36535,69 @@ function computeReplyRecipients(kind, headers, flags) {
|
|
|
36046
36535
|
}
|
|
36047
36536
|
return emails;
|
|
36048
36537
|
}
|
|
36049
|
-
|
|
36538
|
+
function metadataThreadId(raw) {
|
|
36539
|
+
try {
|
|
36540
|
+
const threadId = JSON.parse(raw)?.message?.threadId;
|
|
36541
|
+
return typeof threadId === "string" ? threadId : void 0;
|
|
36542
|
+
} catch {
|
|
36543
|
+
return void 0;
|
|
36544
|
+
}
|
|
36545
|
+
}
|
|
36546
|
+
function replyTokenSubject(kind, messageId, account, headers, threadId, flags) {
|
|
36547
|
+
const removed = new Set(extractEmails(...flags.remove ?? []));
|
|
36548
|
+
const keep = (emails) => emails.filter((e) => !removed.has(e));
|
|
36549
|
+
const to = keep(extractEmails(headers.from, ...kind === "reply-all" ? [headers.to] : [], ...flags.to ?? []));
|
|
36550
|
+
const cc = keep(extractEmails(...kind === "reply-all" ? [headers.cc] : [], ...flags.cc ?? []));
|
|
36551
|
+
const bcc = keep(extractEmails(...flags.bcc ?? []));
|
|
36552
|
+
const attachments = attachmentDetails(flags.attach, flags.attachInline);
|
|
36553
|
+
const subject = flags.subject || (headers.subject ? `Re: ${headers.subject}` : void 0);
|
|
36554
|
+
const from = flags.from ?? (flags.autoFromAddressedAlias ? "the send-as alias the original was addressed to" : senderPreview(account));
|
|
36555
|
+
const localFiles = attachmentDetails([flags.bodyHtmlFile, flags.signatureFile].filter((p) => Boolean(p)), void 0);
|
|
36556
|
+
return {
|
|
36557
|
+
target: messageId,
|
|
36558
|
+
payload: {
|
|
36559
|
+
kind,
|
|
36560
|
+
to,
|
|
36561
|
+
cc,
|
|
36562
|
+
bcc,
|
|
36563
|
+
subject,
|
|
36564
|
+
from,
|
|
36565
|
+
body: flags.body,
|
|
36566
|
+
bodyHtml: flags.bodyHtml,
|
|
36567
|
+
localFiles,
|
|
36568
|
+
signature: Boolean(flags.signature),
|
|
36569
|
+
signatureFrom: flags.signatureFrom,
|
|
36570
|
+
attachments,
|
|
36571
|
+
quote: !flags.noQuote,
|
|
36572
|
+
threadId,
|
|
36573
|
+
inReplyTo: headers.message_id,
|
|
36574
|
+
references: headers.references
|
|
36575
|
+
},
|
|
36576
|
+
preview: {
|
|
36577
|
+
from,
|
|
36578
|
+
to,
|
|
36579
|
+
cc,
|
|
36580
|
+
bcc,
|
|
36581
|
+
subject,
|
|
36582
|
+
body: flags.body,
|
|
36583
|
+
...flags.bodyHtml ? { bodyHtml: flags.bodyHtml } : {},
|
|
36584
|
+
...flags.bodyHtmlFile ? { bodyHtmlFile: flags.bodyHtmlFile } : {},
|
|
36585
|
+
...flags.signature || flags.signatureFrom || flags.signatureFile ? { signature: flags.signatureFile ?? flags.signatureFrom ?? "the Gmail signature of the sending address" } : {},
|
|
36586
|
+
quotesOriginal: !flags.noQuote,
|
|
36587
|
+
attachments: attachmentPreview(attachments),
|
|
36588
|
+
replyingToMessageId: messageId,
|
|
36589
|
+
threadId,
|
|
36590
|
+
inReplyTo: headers.message_id
|
|
36591
|
+
}
|
|
36592
|
+
};
|
|
36593
|
+
}
|
|
36594
|
+
async function sendReply(kind, toolName, messageId, account, flags, ctx, confirmToken) {
|
|
36050
36595
|
const metaResult = await runOrDiagnose(["gmail", "get", pos(messageId), "--format=metadata"], { account });
|
|
36051
36596
|
if (metaResult.isError) return metaResult;
|
|
36052
|
-
const
|
|
36597
|
+
const metaText = resultText(metaResult);
|
|
36598
|
+
const headers = parseMetadataHeaders(metaText);
|
|
36053
36599
|
const recipients = computeReplyRecipients(kind, headers, flags);
|
|
36054
|
-
const confirmation = requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
|
|
36600
|
+
const confirmation = await requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
|
|
36055
36601
|
messageId,
|
|
36056
36602
|
recipients,
|
|
36057
36603
|
recipientCount: recipients.length,
|
|
@@ -36062,6 +36608,13 @@ async function sendReply(kind, toolName, messageId, account, flags, ctx) {
|
|
|
36062
36608
|
bodyHtmlFile: flags.bodyHtmlFile,
|
|
36063
36609
|
attachmentCount: (flags.attach?.length ?? 0) + (flags.attachInline?.length ?? 0),
|
|
36064
36610
|
attachments: attachmentNames(flags.attach, flags.attachInline)
|
|
36611
|
+
}, {
|
|
36612
|
+
tool: toolName,
|
|
36613
|
+
account,
|
|
36614
|
+
confirmToken,
|
|
36615
|
+
// The metadata read above IS the phase-2 re-read: it runs on every call,
|
|
36616
|
+
// so the original's Message-ID/References are fresh here.
|
|
36617
|
+
subject: () => replyTokenSubject(kind, messageId, account, headers, metadataThreadId(metaText), flags)
|
|
36065
36618
|
});
|
|
36066
36619
|
if (confirmation) return confirmation;
|
|
36067
36620
|
const args = ["gmail", kind, pos(messageId)];
|
|
@@ -36079,7 +36632,7 @@ function vetGmailRun(subcommand, args) {
|
|
|
36079
36632
|
return `gog gmail ${subcommand} can forward or hand over mail and is not available through gog_gmail_run. Use the dedicated gog_gmail_* tool instead.`;
|
|
36080
36633
|
}
|
|
36081
36634
|
if (subcommand === "settings") {
|
|
36082
|
-
const blocked = args
|
|
36635
|
+
const blocked = hasCommandWord(args, GMAIL_RUN_BLOCKED_SETTINGS);
|
|
36083
36636
|
if (blocked) {
|
|
36084
36637
|
return `gog gmail settings ${blocked} can forward or hand over mail and is not available through gog_gmail_run. Use the dedicated gog_gmail_* tool instead.`;
|
|
36085
36638
|
}
|
|
@@ -36138,7 +36691,7 @@ function registerGmailTools(server) {
|
|
|
36138
36691
|
return runOrDiagnose(args, { account });
|
|
36139
36692
|
});
|
|
36140
36693
|
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.',
|
|
36694
|
+
description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the recipients, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. Two ways to attach a file: `attach` takes paths READ ON THE GOG SERVER, and `attachInline` takes the bytes themselves. Use attachInline unless you know the file exists on the same machine gog runs on \u2014 on the hosted connector and any remote deployment there is no shared filesystem, so no path you can name resolves there and `attach` will fail with "no such file or directory". When either is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were embedded. NOT the tool for answering a message: replyToMessageId only files this in the right thread \u2014 the subject, recipients and body are entirely yours, and the original is not quoted unless you set quote. Use gog_gmail_reply / gog_gmail_reply_all instead, which inherit all three.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36142
36695
|
annotations: { destructiveHint: true },
|
|
36143
36696
|
inputSchema: external_exports.object({
|
|
36144
36697
|
to: external_exports.string().describe("Recipient(s), comma-separated"),
|
|
@@ -36151,9 +36704,10 @@ function registerGmailTools(server) {
|
|
|
36151
36704
|
quote: external_exports.boolean().optional().describe("Include the original message quoted below the body. Requires replyToMessageId or threadId. gog quotes by DEFAULT on gmail reply but never on gmail send, so without this a threaded send arrives with the original nowhere in it."),
|
|
36152
36705
|
attach: external_exports.array(external_exports.string()).optional().describe(`File paths to attach (repeatable), resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on a hosted deployment (e.g. mcp-host) these paths do not exist and the call fails with "no such file or directory" \u2014 use attachInline there. Each file is read on the server, base64-encoded with a MIME type inferred from its extension, and added as a multipart attachment. Must be inside the server's GOG_FILE_ROOTS directories (default ~/gogcli-mcp-files).`),
|
|
36153
36706
|
attachInline: attachInlineParam,
|
|
36154
|
-
account: accountParam
|
|
36707
|
+
account: accountParam,
|
|
36708
|
+
confirmToken: confirmTokenParam
|
|
36155
36709
|
})
|
|
36156
|
-
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account }, ctx) => {
|
|
36710
|
+
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account, confirmToken }, ctx) => {
|
|
36157
36711
|
confinePaths(attach, "attach");
|
|
36158
36712
|
const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
|
|
36159
36713
|
if (cc) args.push(`--cc=${cc}`);
|
|
@@ -36165,7 +36719,7 @@ function registerGmailTools(server) {
|
|
|
36165
36719
|
const inline = inlineAttachmentArgs("attach", attachInline, args);
|
|
36166
36720
|
args.push(...inline);
|
|
36167
36721
|
const recipients = extractEmails(to, cc, bcc);
|
|
36168
|
-
const confirmation = requireGmailDispatchConfirmation(ctx, "gmail.send", {
|
|
36722
|
+
const confirmation = await requireGmailDispatchConfirmation(ctx, "gmail.send", {
|
|
36169
36723
|
to,
|
|
36170
36724
|
cc,
|
|
36171
36725
|
bcc,
|
|
@@ -36178,6 +36732,32 @@ function registerGmailTools(server) {
|
|
|
36178
36732
|
quoting: Boolean(quote),
|
|
36179
36733
|
attachmentCount: (attach?.length ?? 0) + (attachInline?.length ?? 0),
|
|
36180
36734
|
attachments: attachmentNames(attach, attachInline)
|
|
36735
|
+
}, {
|
|
36736
|
+
tool: "gog_gmail_send",
|
|
36737
|
+
account,
|
|
36738
|
+
confirmToken,
|
|
36739
|
+
// Nothing is stored between the phases: the payload IS the arguments,
|
|
36740
|
+
// so phase 2 must repeat them and any difference is a changed send.
|
|
36741
|
+
subject: () => {
|
|
36742
|
+
const attachments = attachmentDetails(attach, attachInline);
|
|
36743
|
+
const from = senderPreview(account);
|
|
36744
|
+
return {
|
|
36745
|
+
target: replyToMessageId ?? threadId ?? "",
|
|
36746
|
+
payload: { from, to, cc, bcc, subject, body, attachments, threadId, inReplyTo: replyToMessageId, quote: Boolean(quote) },
|
|
36747
|
+
preview: {
|
|
36748
|
+
from,
|
|
36749
|
+
to,
|
|
36750
|
+
cc,
|
|
36751
|
+
bcc,
|
|
36752
|
+
subject,
|
|
36753
|
+
body,
|
|
36754
|
+
attachments: attachmentPreview(attachments),
|
|
36755
|
+
threadId,
|
|
36756
|
+
inReplyTo: replyToMessageId,
|
|
36757
|
+
quotesOriginal: Boolean(quote)
|
|
36758
|
+
}
|
|
36759
|
+
};
|
|
36760
|
+
}
|
|
36181
36761
|
});
|
|
36182
36762
|
if (confirmation) return confirmation;
|
|
36183
36763
|
const result = await runOrDiagnose(args, { account });
|
|
@@ -36185,22 +36765,22 @@ function registerGmailTools(server) {
|
|
|
36185
36765
|
return result;
|
|
36186
36766
|
});
|
|
36187
36767
|
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.',
|
|
36768
|
+
description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipient, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. To STAGE a reply instead of sending it, use gog_gmail_drafts_reply (gogcli-mcp-gmail only), which never needs confirmation. Reply to a Gmail message (goes to the original sender only). USE THIS, not gog_gmail_send, whenever you are answering a message: it threads off the original AND inherits its "Re:" subject and quotes its body below yours, which gog_gmail_send does not \u2014 a send with replyToMessageId lands in the right thread but reads as a brand-new message, with the original nowhere in it. To answer every participant use gog_gmail_reply_all. The gogcli-mcp-gmail package adds two more routes with the same composition: gog_gmail_autoreply to reply across every message matching a query, and gog_gmail_drafts_reply to stage this exact reply as a draft instead of sending it.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36189
36769
|
annotations: { destructiveHint: true },
|
|
36190
36770
|
inputSchema: sendReplySchema
|
|
36191
|
-
}, async ({ messageId, account, ...flags }, ctx) => {
|
|
36771
|
+
}, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
|
|
36192
36772
|
assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
|
|
36193
36773
|
confineReplyPaths(flags);
|
|
36194
|
-
return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx);
|
|
36774
|
+
return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx, confirmToken);
|
|
36195
36775
|
});
|
|
36196
36776
|
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.',
|
|
36777
|
+
description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipients, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. To STAGE a reply-all instead of sending it, use gog_gmail_drafts_reply_all (gogcli-mcp-gmail only), which never needs confirmation. Reply to all participants of a Gmail message (the sender plus every To/Cc recipient). Same inherited "Re:" subject and quoted original as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36198
36778
|
annotations: { destructiveHint: true },
|
|
36199
36779
|
inputSchema: sendReplySchema
|
|
36200
|
-
}, async ({ messageId, account, ...flags }, ctx) => {
|
|
36780
|
+
}, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
|
|
36201
36781
|
assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
|
|
36202
36782
|
confineReplyPaths(flags);
|
|
36203
|
-
return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx);
|
|
36783
|
+
return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx, confirmToken);
|
|
36204
36784
|
});
|
|
36205
36785
|
registerRunTool(server, {
|
|
36206
36786
|
service: "gmail",
|
|
@@ -36541,7 +37121,7 @@ function registerTasksTools(server) {
|
|
|
36541
37121
|
}
|
|
36542
37122
|
|
|
36543
37123
|
// src/server.ts
|
|
36544
|
-
var VERSION = true ? "4.
|
|
37124
|
+
var VERSION = true ? "4.4.0" : "0.0.0";
|
|
36545
37125
|
var BASE_TOOL_REGISTRARS = [
|
|
36546
37126
|
registerApiTools,
|
|
36547
37127
|
registerAppScriptTools,
|
|
@@ -36593,12 +37173,12 @@ async function bootstrapGogAuth(env = process.env, options = {}) {
|
|
|
36593
37173
|
const [clientId, clientSecret, refreshToken, account] = values;
|
|
36594
37174
|
const { spawner } = options;
|
|
36595
37175
|
try {
|
|
36596
|
-
const { createHash:
|
|
37176
|
+
const { createHash: createHash4 } = await import("node:crypto");
|
|
36597
37177
|
const { mkdir, readFile, writeFile, chmod } = await import("node:fs/promises");
|
|
36598
37178
|
const home = options.home ?? (await import("node:os")).homedir();
|
|
36599
37179
|
const dir = join5(home, ".gogcli-mcp");
|
|
36600
37180
|
const marker = join5(dir, AUTH_BOOTSTRAP_MARKER);
|
|
36601
|
-
const fingerprint =
|
|
37181
|
+
const fingerprint = createHash4("sha256").update(values.join("\0")).digest("hex");
|
|
36602
37182
|
const previous = await readFile(marker, "utf8").catch(() => void 0);
|
|
36603
37183
|
if (previous === fingerprint && await accountListed(account, spawner)) return "present";
|
|
36604
37184
|
await run(
|
|
@@ -36623,7 +37203,7 @@ async function bootstrapGogAuth(env = process.env, options = {}) {
|
|
|
36623
37203
|
}
|
|
36624
37204
|
|
|
36625
37205
|
// src/blob-urls.ts
|
|
36626
|
-
import { createHmac as
|
|
37206
|
+
import { createHmac as createHmac3 } from "node:crypto";
|
|
36627
37207
|
var BLOB_URL_MAX_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
36628
37208
|
var BLOB_URL_CEILING_MARGIN_MS = 60 * 1e3;
|
|
36629
37209
|
var BLOB_URL_DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
@@ -36635,8 +37215,8 @@ ${exp}`;
|
|
|
36635
37215
|
function writePayload(objectKey, contentType, exp) {
|
|
36636
37216
|
return `put\0${objectKey}\0${contentType}\0${exp}`;
|
|
36637
37217
|
}
|
|
36638
|
-
function
|
|
36639
|
-
return
|
|
37218
|
+
function sign2(signingKey, payload) {
|
|
37219
|
+
return createHmac3("sha256", signingKey).update(payload, "utf8").digest("base64url");
|
|
36640
37220
|
}
|
|
36641
37221
|
function objectPathSegments(rest) {
|
|
36642
37222
|
const segments = rest.split("/");
|
|
@@ -36672,7 +37252,7 @@ function createBlobUrlMinter(config2) {
|
|
|
36672
37252
|
function mint(rest, exp, payload) {
|
|
36673
37253
|
const segments = objectPathSegments(rest);
|
|
36674
37254
|
const url2 = `${basePrefix}/${segments.map(encodeURIComponent).join("/")}`;
|
|
36675
|
-
const signature =
|
|
37255
|
+
const signature = sign2(config2.signingKey, payload(`${registrationId}/${segments.join("/")}`));
|
|
36676
37256
|
return `${url2}?exp=${exp}&sig=${signature}`;
|
|
36677
37257
|
}
|
|
36678
37258
|
return {
|
|
@@ -36821,6 +37401,7 @@ export {
|
|
|
36821
37401
|
BLOB_UPLOAD_TIMEOUT_MS,
|
|
36822
37402
|
BLOB_URL_DEFAULT_TTL_MS,
|
|
36823
37403
|
BLOB_URL_MAX_TTL_MS,
|
|
37404
|
+
CONFIRM_FALLBACK_DESCRIPTION,
|
|
36824
37405
|
FILE_ROOTS_ENV,
|
|
36825
37406
|
INLINE_ATTACHMENT_LIMITS_TEXT,
|
|
36826
37407
|
MAX_BLOB_UPLOAD_BYTES,
|
|
@@ -36835,7 +37416,9 @@ export {
|
|
|
36835
37416
|
appendReplyFlags,
|
|
36836
37417
|
assertNotBoth,
|
|
36837
37418
|
attachInlineParam,
|
|
37419
|
+
attachmentDetails,
|
|
36838
37420
|
attachmentNames,
|
|
37421
|
+
attachmentPreview,
|
|
36839
37422
|
authToolsFor,
|
|
36840
37423
|
blobStoreFromEnv,
|
|
36841
37424
|
bodyPreview,
|
|
@@ -36843,6 +37426,7 @@ export {
|
|
|
36843
37426
|
confineAtFile,
|
|
36844
37427
|
confinePath,
|
|
36845
37428
|
confinePaths,
|
|
37429
|
+
confirmTokenParam,
|
|
36846
37430
|
createBlobUrlMinter,
|
|
36847
37431
|
defaultFileRoot,
|
|
36848
37432
|
diagnose,
|
|
@@ -36865,6 +37449,7 @@ export {
|
|
|
36865
37449
|
pos,
|
|
36866
37450
|
prepareDownloadRoot,
|
|
36867
37451
|
pushPaginationFlags,
|
|
37452
|
+
readCourse,
|
|
36868
37453
|
registerApiTools,
|
|
36869
37454
|
registerAppScriptTools,
|
|
36870
37455
|
registerAuthTools,
|
|
@@ -36881,12 +37466,14 @@ export {
|
|
|
36881
37466
|
registerTasksTools,
|
|
36882
37467
|
removeDownload,
|
|
36883
37468
|
replySchema,
|
|
37469
|
+
requireDispatchConfirmation,
|
|
36884
37470
|
requireGmailDispatchConfirmation,
|
|
36885
37471
|
resolvePageToken,
|
|
36886
37472
|
resultText,
|
|
36887
37473
|
run,
|
|
36888
37474
|
runBinary,
|
|
36889
37475
|
runOrDiagnose,
|
|
37476
|
+
senderPreview,
|
|
36890
37477
|
stripConsumedPageToken,
|
|
36891
37478
|
uploadToBlobStore
|
|
36892
37479
|
};
|