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/index.js
CHANGED
|
@@ -34206,14 +34206,14 @@ function serveStdio(factory, options = {}) {
|
|
|
34206
34206
|
return false;
|
|
34207
34207
|
};
|
|
34208
34208
|
const answerLegacyRejection = (request, reason, requestedVersion) => {
|
|
34209
|
-
const
|
|
34209
|
+
const rejection3 = modernOnlyStrictRejection({
|
|
34210
34210
|
kind: "legacy",
|
|
34211
34211
|
reason,
|
|
34212
34212
|
...requestedVersion !== void 0 && { requestedVersion }
|
|
34213
34213
|
}, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
34214
|
-
if (
|
|
34215
|
-
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${
|
|
34216
|
-
return writeErrorResponse(request.id,
|
|
34214
|
+
if (rejection3 === void 0) return Promise.resolve();
|
|
34215
|
+
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection3.cell}): ${rejection3.message}`));
|
|
34216
|
+
return writeErrorResponse(request.id, rejection3.code, rejection3.message, rejection3.data);
|
|
34217
34217
|
};
|
|
34218
34218
|
const onInstanceClosed = (channel) => {
|
|
34219
34219
|
if (closing || channel === discarding) return;
|
|
@@ -34995,9 +34995,9 @@ function offsetAt(instant, tz) {
|
|
|
34995
34995
|
const w = wallPartsIn(instant, tz);
|
|
34996
34996
|
const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
|
|
34997
34997
|
const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
|
|
34998
|
-
const
|
|
34998
|
+
const sign2 = minutes < 0 ? "-" : "+";
|
|
34999
34999
|
const abs = Math.abs(minutes);
|
|
35000
|
-
return `${
|
|
35000
|
+
return `${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
35001
35001
|
}
|
|
35002
35002
|
function wallPartsIn(instant, tz) {
|
|
35003
35003
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
@@ -35843,13 +35843,239 @@ function assertNotBoth(inlineParam, fileParam, inlineValue, fileValue) {
|
|
|
35843
35843
|
}
|
|
35844
35844
|
}
|
|
35845
35845
|
|
|
35846
|
+
// src/dispatch-confirmation.ts
|
|
35847
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
35848
|
+
import { readFileSync } from "node:fs";
|
|
35849
|
+
|
|
35850
|
+
// src/send-confirm-token.ts
|
|
35851
|
+
import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
35852
|
+
var CONFIRM_TOKEN_TTL_DEFAULT_SECONDS = 600;
|
|
35853
|
+
var PREFIX = "gct1";
|
|
35854
|
+
var secret;
|
|
35855
|
+
var spent = /* @__PURE__ */ new Map();
|
|
35856
|
+
function sendConfirmFallbackEnabled() {
|
|
35857
|
+
return readEnvVar("GOG_SEND_CONFIRM_FALLBACK")?.trim().toLowerCase() === "token";
|
|
35858
|
+
}
|
|
35859
|
+
function confirmTokenTtlSeconds() {
|
|
35860
|
+
const raw = readEnvVar("GOG_CONFIRM_TTL_SECONDS")?.trim();
|
|
35861
|
+
if (raw && /^\d+$/.test(raw)) {
|
|
35862
|
+
const n = Number(raw);
|
|
35863
|
+
if (n > 0) return n;
|
|
35864
|
+
}
|
|
35865
|
+
return CONFIRM_TOKEN_TTL_DEFAULT_SECONDS;
|
|
35866
|
+
}
|
|
35867
|
+
function hmacKey() {
|
|
35868
|
+
if (!secret) {
|
|
35869
|
+
const configured = readEnvVar("GOG_CONFIRM_SECRET");
|
|
35870
|
+
secret = configured ? Buffer.from(configured, "utf8") : randomBytes(32);
|
|
35871
|
+
}
|
|
35872
|
+
return secret;
|
|
35873
|
+
}
|
|
35874
|
+
function canonicalize(value) {
|
|
35875
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
35876
|
+
if (value && typeof value === "object") {
|
|
35877
|
+
const out = {};
|
|
35878
|
+
for (const key of Object.keys(value).sort()) {
|
|
35879
|
+
const v = value[key];
|
|
35880
|
+
if (v !== void 0) out[key] = canonicalize(v);
|
|
35881
|
+
}
|
|
35882
|
+
return out;
|
|
35883
|
+
}
|
|
35884
|
+
return value;
|
|
35885
|
+
}
|
|
35886
|
+
function hashSendPayload(payload) {
|
|
35887
|
+
return createHash2("sha256").update(JSON.stringify(canonicalize(payload))).digest("hex");
|
|
35888
|
+
}
|
|
35889
|
+
function sign(body) {
|
|
35890
|
+
return createHmac2("sha256", hmacKey()).update(`${PREFIX}.${body}`).digest("base64url");
|
|
35891
|
+
}
|
|
35892
|
+
function issueConfirmToken(binding, now = Date.now()) {
|
|
35893
|
+
const exp = now + confirmTokenTtlSeconds() * 1e3;
|
|
35894
|
+
const claims = {
|
|
35895
|
+
t: binding.tool,
|
|
35896
|
+
a: binding.account,
|
|
35897
|
+
g: binding.target,
|
|
35898
|
+
...binding.revision === void 0 ? {} : { r: binding.revision },
|
|
35899
|
+
h: binding.payloadHash,
|
|
35900
|
+
iat: now,
|
|
35901
|
+
exp,
|
|
35902
|
+
n: randomBytes(16).toString("base64url")
|
|
35903
|
+
};
|
|
35904
|
+
const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
|
|
35905
|
+
return { token: `${PREFIX}.${body}.${sign(body)}`, expiresAt: new Date(exp).toISOString() };
|
|
35906
|
+
}
|
|
35907
|
+
function signatureMatches(body, sig) {
|
|
35908
|
+
const expected = Buffer.from(sign(body), "utf8");
|
|
35909
|
+
const actual = Buffer.from(sig, "utf8");
|
|
35910
|
+
return expected.length === actual.length && timingSafeEqual2(expected, actual);
|
|
35911
|
+
}
|
|
35912
|
+
function parseClaims(body) {
|
|
35913
|
+
try {
|
|
35914
|
+
return JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
35915
|
+
} catch {
|
|
35916
|
+
return void 0;
|
|
35917
|
+
}
|
|
35918
|
+
}
|
|
35919
|
+
function verifyConfirmToken(token, binding, now = Date.now()) {
|
|
35920
|
+
for (const [nonce, exp] of spent) if (exp < now) spent.delete(nonce);
|
|
35921
|
+
const parts = token.split(".");
|
|
35922
|
+
if (parts.length !== 3 || parts[0] !== PREFIX || !parts[1] || !parts[2]) return { ok: false, error: "TOKEN_INVALID" };
|
|
35923
|
+
const [, body, sig] = parts;
|
|
35924
|
+
if (!signatureMatches(body, sig)) return { ok: false, error: "TOKEN_INVALID" };
|
|
35925
|
+
const claims = parseClaims(body);
|
|
35926
|
+
if (!claims) return { ok: false, error: "TOKEN_INVALID" };
|
|
35927
|
+
if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
|
|
35928
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
35929
|
+
}
|
|
35930
|
+
if (spent.has(claims.n)) return { ok: false, error: "TOKEN_REUSED" };
|
|
35931
|
+
if (now > claims.exp) return { ok: false, error: "TOKEN_EXPIRED" };
|
|
35932
|
+
if (claims.r !== binding.revision) return { ok: false, error: "DRAFT_CHANGED", reason: "message-id-rotated" };
|
|
35933
|
+
if (claims.h !== binding.payloadHash) return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
|
|
35934
|
+
spent.set(claims.n, claims.exp);
|
|
35935
|
+
return { ok: true };
|
|
35936
|
+
}
|
|
35937
|
+
|
|
35938
|
+
// src/dispatch-confirmation.ts
|
|
35939
|
+
var BODY_PREVIEW_MAX = 2048;
|
|
35940
|
+
function bodyPreview(text) {
|
|
35941
|
+
if (!text) return void 0;
|
|
35942
|
+
if (text.length <= BODY_PREVIEW_MAX) return text;
|
|
35943
|
+
return `${text.slice(0, BODY_PREVIEW_MAX)}\u2026 [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
|
|
35944
|
+
}
|
|
35945
|
+
function attachmentNames(paths, inline) {
|
|
35946
|
+
return [...paths ?? [], ...(inline ?? []).map((a) => a.filename)];
|
|
35947
|
+
}
|
|
35948
|
+
function attachmentDetails(paths, inline) {
|
|
35949
|
+
const out = [];
|
|
35950
|
+
for (const path of paths ?? []) {
|
|
35951
|
+
let bytes;
|
|
35952
|
+
try {
|
|
35953
|
+
bytes = readFileSync(path);
|
|
35954
|
+
} catch {
|
|
35955
|
+
bytes = void 0;
|
|
35956
|
+
}
|
|
35957
|
+
out.push(bytes ? { name: path, size: bytes.length, sha256: createHash3("sha256").update(bytes).digest("hex") } : { name: path, size: null });
|
|
35958
|
+
}
|
|
35959
|
+
for (const a of inline ?? []) {
|
|
35960
|
+
out.push({
|
|
35961
|
+
name: a.filename,
|
|
35962
|
+
size: Buffer.from(a.contentBase64, "base64").length,
|
|
35963
|
+
sha256: createHash3("sha256").update(a.contentBase64).digest("hex")
|
|
35964
|
+
});
|
|
35965
|
+
}
|
|
35966
|
+
return out;
|
|
35967
|
+
}
|
|
35968
|
+
function senderPreview(account, from) {
|
|
35969
|
+
return from ?? account ?? readEnvVar("GOG_ACCOUNT") ?? "the gog account's default address";
|
|
35970
|
+
}
|
|
35971
|
+
function attachmentPreview(details) {
|
|
35972
|
+
return details.map(({ name, size }) => ({ name, size }));
|
|
35973
|
+
}
|
|
35974
|
+
var CONFIRM_SEND_INSTRUCTION = "Show this preview to the user verbatim and send only after they explicitly approve in chat. Then call again with confirmToken.";
|
|
35975
|
+
var CONFIRM_ACTION_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
|
|
35976
|
+
var FALLBACK_HINT = "Or set GOG_SEND_CONFIRM_FALLBACK=token to enable two-step confirmation.";
|
|
35977
|
+
var CONFIRM_FALLBACK_DESCRIPTION = ' If the client cannot show that prompt (no MCP elicitation, e.g. claude.ai) and the server sets GOG_SEND_CONFIRM_FALLBACK=token, a two-step flow applies instead: call WITHOUT confirmToken and nothing is sent or changed \u2014 the result has status "confirmation-required", the full preview and a confirmToken. Show that preview to the user verbatim; only after they explicitly approve it in chat, call again with the SAME arguments plus confirmToken. The tool re-reads what it would act on and refuses (DRAFT_CHANGED, with a fresh preview and token) if it changed; TOKEN_EXPIRED / TOKEN_REUSED / TOKEN_INVALID also do nothing.';
|
|
35978
|
+
var confirmTokenParam = external_exports.string().optional().describe(
|
|
35979
|
+
`ONLY for the two-step fallback (client without MCP elicitation, server with GOG_SEND_CONFIRM_FALLBACK=token). The confirmToken from this same tool's phase-1 "confirmation-required" response, passed back ONLY after the user has seen that preview and explicitly approved it in chat \u2014 never on the first call, never invented, never reused. Call again with the same arguments. Ignored when the client supports elicitation.`
|
|
35980
|
+
);
|
|
35981
|
+
var TOKEN_ERROR_NOTE = {
|
|
35982
|
+
TOKEN_EXPIRED: "Nothing was sent or changed: the confirmToken expired. Call again WITHOUT confirmToken for a fresh preview, and ask the user to approve it again.",
|
|
35983
|
+
TOKEN_REUSED: "Nothing was sent or changed by this call: this confirmToken was already used, and one approval acts once. If doing it again is really intended, call again WITHOUT confirmToken and get a new approval.",
|
|
35984
|
+
TOKEN_INVALID: "Nothing was sent or changed: this confirmToken was not issued by this server for this tool, account and target (or the server has restarted since). Call again WITHOUT confirmToken for a fresh preview and approval."
|
|
35985
|
+
};
|
|
35986
|
+
var DRAFT_CHANGED_NOTE = {
|
|
35987
|
+
"message-id-rotated": "Nothing was sent or changed: the target was edited since the user approved it (a draft's messageId or an event's version rotated), so what would happen is not what they saw.",
|
|
35988
|
+
"payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
|
|
35989
|
+
};
|
|
35990
|
+
function isToolResult(value) {
|
|
35991
|
+
return Array.isArray(value.content);
|
|
35992
|
+
}
|
|
35993
|
+
function rejection2(data) {
|
|
35994
|
+
return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
|
|
35995
|
+
}
|
|
35996
|
+
async function tokenConfirmation(op, fallback) {
|
|
35997
|
+
const subject = await fallback.subject();
|
|
35998
|
+
if (isToolResult(subject)) return subject;
|
|
35999
|
+
const binding = {
|
|
36000
|
+
tool: fallback.tool,
|
|
36001
|
+
account: fallback.account ?? readEnvVar("GOG_ACCOUNT") ?? "",
|
|
36002
|
+
target: subject.target,
|
|
36003
|
+
...subject.revision === void 0 ? {} : { revision: subject.revision },
|
|
36004
|
+
payloadHash: hashSendPayload(subject.payload)
|
|
36005
|
+
};
|
|
36006
|
+
const phaseOne = () => {
|
|
36007
|
+
const { token, expiresAt } = issueConfirmToken(binding);
|
|
36008
|
+
return {
|
|
36009
|
+
action: op,
|
|
36010
|
+
preview: subject.preview,
|
|
36011
|
+
confirmToken: token,
|
|
36012
|
+
expiresAt,
|
|
36013
|
+
ttlSeconds: confirmTokenTtlSeconds(),
|
|
36014
|
+
instruction: fallback.instruction ?? CONFIRM_ACTION_INSTRUCTION
|
|
36015
|
+
};
|
|
36016
|
+
};
|
|
36017
|
+
if (!fallback.confirmToken) {
|
|
36018
|
+
return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
|
|
36019
|
+
}
|
|
36020
|
+
const verdict = verifyConfirmToken(fallback.confirmToken, binding);
|
|
36021
|
+
if (verdict.ok) return void 0;
|
|
36022
|
+
if (verdict.error === "DRAFT_CHANGED") {
|
|
36023
|
+
return rejection2({
|
|
36024
|
+
error: "DRAFT_CHANGED",
|
|
36025
|
+
reason: verdict.reason,
|
|
36026
|
+
note: `${DRAFT_CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
|
|
36027
|
+
...phaseOne()
|
|
36028
|
+
});
|
|
36029
|
+
}
|
|
36030
|
+
return rejection2({ error: verdict.error, action: op, note: TOKEN_ERROR_NOTE[verdict.error] });
|
|
36031
|
+
}
|
|
36032
|
+
function gatedElsewhere(what, via, does, tool) {
|
|
36033
|
+
return `${what} ${does} and is not available through ${via}. Use ${tool}, which asks the user to confirm.`;
|
|
36034
|
+
}
|
|
36035
|
+
function hasCommandWord(args, words) {
|
|
36036
|
+
return args.find((a) => words.has(a.toLowerCase()));
|
|
36037
|
+
}
|
|
36038
|
+
async function requireDispatchConfirmation(ctx, options) {
|
|
36039
|
+
const { action, fallback } = options;
|
|
36040
|
+
if (fallback && callerAcceptsFormElicitation(ctx) === false && sendConfirmFallbackEnabled()) {
|
|
36041
|
+
return tokenConfirmation(action, fallback);
|
|
36042
|
+
}
|
|
36043
|
+
const note = fallback ? [options.unsupportedNote, FALLBACK_HINT].filter(Boolean).join(" ") : options.unsupportedNote;
|
|
36044
|
+
return requireConfirmation(ctx, {
|
|
36045
|
+
action,
|
|
36046
|
+
message: options.message,
|
|
36047
|
+
details: options.details,
|
|
36048
|
+
confirmationLabel: options.confirmationLabel,
|
|
36049
|
+
...note ? { unsupportedNote: note } : {}
|
|
36050
|
+
});
|
|
36051
|
+
}
|
|
36052
|
+
function resultText(result) {
|
|
36053
|
+
const first = result.content[0];
|
|
36054
|
+
return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
|
|
36055
|
+
}
|
|
36056
|
+
|
|
35846
36057
|
// src/tools/api.ts
|
|
35847
36058
|
var GMAIL_API_BLOCKED = /(?:\.send$|forwarding|filters\.create|filters\.update|delegates\.create)/i;
|
|
36059
|
+
var DISPATCH_API_BLOCKED = {
|
|
36060
|
+
chat: [{ method: /(?:^|\.)spaces\.messages\.create$/i, does: "posts a Chat message", tool: "gog_chat_messages_send / gog_chat_dm_send" }],
|
|
36061
|
+
drive: [{ method: /(?:^|\.)permissions\.(?:create|update)$/i, does: "grants access to a file", tool: "gog_drive_share" }],
|
|
36062
|
+
classroom: [
|
|
36063
|
+
{ method: /(?:^|\.)courses\.announcements\.create$/i, does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
36064
|
+
{ method: /(?:^|\.)invitations\.create$/i, does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
|
|
36065
|
+
],
|
|
36066
|
+
calendar: [
|
|
36067
|
+
{ method: /(?:^|\.)events\.(?:insert|import|quickadd)$/i, does: "can put an event on guests' calendars", tool: "gog_calendar_create" },
|
|
36068
|
+
{ method: /(?:^|\.)events\.(?:update|patch)$/i, does: "can change what guests see", tool: "gog_calendar_update / gog_calendar_respond" }
|
|
36069
|
+
]
|
|
36070
|
+
};
|
|
35848
36071
|
function refusedApiCall(api, method) {
|
|
35849
|
-
|
|
36072
|
+
const name = api.trim().toLowerCase();
|
|
36073
|
+
const m = method.trim();
|
|
36074
|
+
if (name === "gmail" && GMAIL_API_BLOCKED.test(m)) {
|
|
35850
36075
|
return `gmail ${method} is not available through gog_api_call: it sends or forwards mail. Use gog_gmail_send / gog_gmail_drafts_send (which ask the user to confirm) or the dedicated gog_gmail_* tool.`;
|
|
35851
36076
|
}
|
|
35852
|
-
|
|
36077
|
+
const hit = (Object.hasOwn(DISPATCH_API_BLOCKED, name) ? DISPATCH_API_BLOCKED[name] : []).find((b) => b.method.test(m));
|
|
36078
|
+
return hit ? gatedElsewhere(`${name} ${m}`, "gog_api_call", hit.does, hit.tool) : void 0;
|
|
35853
36079
|
}
|
|
35854
36080
|
function registerApiTools(server) {
|
|
35855
36081
|
server.registerTool("gog_api_list", {
|
|
@@ -35879,7 +36105,7 @@ function registerApiTools(server) {
|
|
|
35879
36105
|
return runOrDiagnose(args, { account });
|
|
35880
36106
|
});
|
|
35881
36107
|
server.registerTool("gog_api_call", {
|
|
35882
|
-
description: "Call any Discovery-described Google API method directly \u2014 an escape hatch for endpoints gog has no dedicated tool for. Find the exact api/version/method/params with gog_api_describe first. Read methods (GET/LIST) run as-is. Mutating methods (POST/PUT/PATCH/DELETE) are refused unless you set allowWrite=true \u2014 keep it false to preview, or set dryRun=true to print the intended request without sending it. Gmail send and forwarding methods (users.messages.send, users.drafts.send, forwarding/auto-forwarding, filters, delegates) are refused \u2014 use the dedicated gog_gmail_* tools, which ask the user to confirm.",
|
|
36108
|
+
description: "Call any Discovery-described Google API method directly \u2014 an escape hatch for endpoints gog has no dedicated tool for. Find the exact api/version/method/params with gog_api_describe first. Read methods (GET/LIST) run as-is. Mutating methods (POST/PUT/PATCH/DELETE) are refused unless you set allowWrite=true \u2014 keep it false to preview, or set dryRun=true to print the intended request without sending it. Gmail send and forwarding methods (users.messages.send, users.drafts.send, forwarding/auto-forwarding, filters, delegates) are refused \u2014 use the dedicated gog_gmail_* tools, which ask the user to confirm. So are the other methods a dedicated tool asks about: Chat spaces.messages.create, Drive permissions.create/update, Classroom courses.announcements.create and invitations.create, and Calendar events.insert/import/quickAdd/update/patch.",
|
|
35883
36109
|
annotations: { destructiveHint: true },
|
|
35884
36110
|
inputSchema: external_exports.object({
|
|
35885
36111
|
api: external_exports.string().describe("Discovery API name (e.g. drive, gmail, calendar)"),
|
|
@@ -36162,6 +36388,54 @@ function registerAuthTools(server) {
|
|
|
36162
36388
|
}
|
|
36163
36389
|
|
|
36164
36390
|
// src/tools/calendar.ts
|
|
36391
|
+
var CALENDAR_GATED = {
|
|
36392
|
+
create: "gog_calendar_create",
|
|
36393
|
+
add: "gog_calendar_create",
|
|
36394
|
+
new: "gog_calendar_create",
|
|
36395
|
+
update: "gog_calendar_update",
|
|
36396
|
+
edit: "gog_calendar_update",
|
|
36397
|
+
set: "gog_calendar_update",
|
|
36398
|
+
respond: "gog_calendar_respond",
|
|
36399
|
+
rsvp: "gog_calendar_respond",
|
|
36400
|
+
reply: "gog_calendar_respond"
|
|
36401
|
+
};
|
|
36402
|
+
function vetCalendarRun(subcommand, _args) {
|
|
36403
|
+
const sub = subcommand.toLowerCase();
|
|
36404
|
+
const tool = Object.hasOwn(CALENDAR_GATED, sub) ? CALENDAR_GATED[sub] : void 0;
|
|
36405
|
+
return tool ? gatedElsewhere(`gog calendar ${sub}`, "gog_calendar_run", "can change what guests see", tool) : void 0;
|
|
36406
|
+
}
|
|
36407
|
+
function parseAttendees(value) {
|
|
36408
|
+
if (!value) return [];
|
|
36409
|
+
return value.split(",").map((entry) => entry.split(";")).filter((parts) => !parts.slice(1).some((m) => m.trim().toLowerCase() === "resource")).map((parts) => parts[0].trim()).filter(Boolean);
|
|
36410
|
+
}
|
|
36411
|
+
function timeOf(t) {
|
|
36412
|
+
const v = t?.dateTime ?? t?.date;
|
|
36413
|
+
return typeof v === "string" ? v : void 0;
|
|
36414
|
+
}
|
|
36415
|
+
function eventSnapshot(raw) {
|
|
36416
|
+
let event;
|
|
36417
|
+
try {
|
|
36418
|
+
event = JSON.parse(raw)?.event;
|
|
36419
|
+
} catch {
|
|
36420
|
+
event = void 0;
|
|
36421
|
+
}
|
|
36422
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
36423
|
+
return {
|
|
36424
|
+
summary: str(event?.summary),
|
|
36425
|
+
start: timeOf(event?.start),
|
|
36426
|
+
end: timeOf(event?.end),
|
|
36427
|
+
organizer: str(event?.organizer?.email),
|
|
36428
|
+
// Guests are the OTHER people: not this account's own attendee row, not rooms.
|
|
36429
|
+
guests: (Array.isArray(event?.attendees) ? event.attendees : []).filter((a) => a.self !== true && a.resource !== true && typeof a.email === "string").map((a) => a.email),
|
|
36430
|
+
etag: str(event?.etag)
|
|
36431
|
+
};
|
|
36432
|
+
}
|
|
36433
|
+
async function readEvent(calendarId, eventId, account) {
|
|
36434
|
+
const got = await runOrDiagnose(["calendar", "event", pos(calendarId), pos(eventId)], { account });
|
|
36435
|
+
if (got.isError) return { error: got };
|
|
36436
|
+
return { event: eventSnapshot(resultText(got)) };
|
|
36437
|
+
}
|
|
36438
|
+
var CALENDAR_CONFIRM_LABEL = "Confirm that this change, which other people will see, should be made now.";
|
|
36165
36439
|
var reminderParams = {
|
|
36166
36440
|
reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
|
|
36167
36441
|
`Reminders as method:duration, e.g. ["popup:30m", "email:1d"]. Method is popup or email; duration accepts m/h/d (max 40320 minutes = 4 weeks). Google allows at most 5. These REPLACE the event's reminders \u2014 on update, pass an EMPTY array to drop custom reminders and go back to the calendar's defaults. Cannot be combined with noReminders.`
|
|
@@ -36249,7 +36523,7 @@ function registerCalendarTools(server) {
|
|
|
36249
36523
|
return runOrDiagnose(args, { account });
|
|
36250
36524
|
});
|
|
36251
36525
|
server.registerTool("gog_calendar_create", {
|
|
36252
|
-
description: "Create a calendar event. Set withZoom=true to attach a Zoom meeting (requires Zoom S2S OAuth setup via gog_zoom_auth_setup; the join URL + meeting ID + passcode are appended to the event description \u2014 Google rejects native conference card writes from non-Workspace-Marketplace OAuth clients).",
|
|
36526
|
+
description: "Create a calendar event. Set withZoom=true to attach a Zoom meeting (requires Zoom S2S OAuth setup via gog_zoom_auth_setup; the join URL + meeting ID + passcode are appended to the event description \u2014 Google rejects native conference card writes from non-Workspace-Marketplace OAuth clients). With attendees, the event lands on other people's calendars (no invitation email is sent), so the MCP host is asked to show the user a confirmation prompt with the guests, time and details first; a guest-free event is created without asking." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36253
36527
|
annotations: { destructiveHint: false },
|
|
36254
36528
|
inputSchema: external_exports.object({
|
|
36255
36529
|
calendarId: external_exports.string().describe('Calendar ID (use "primary" for the default calendar)'),
|
|
@@ -36263,9 +36537,10 @@ function registerCalendarTools(server) {
|
|
|
36263
36537
|
timezone: external_exports.string().optional().describe("IANA timezone metadata applied to from/to (e.g. America/New_York). Sets both start and end timezone unless start/end timezone are overridden."),
|
|
36264
36538
|
withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)"),
|
|
36265
36539
|
...reminderParams,
|
|
36266
|
-
account: accountParam
|
|
36540
|
+
account: accountParam,
|
|
36541
|
+
confirmToken: confirmTokenParam
|
|
36267
36542
|
})
|
|
36268
|
-
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
|
|
36543
|
+
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account, confirmToken }, ctx) => {
|
|
36269
36544
|
const args = ["calendar", "create", pos(calendarId), `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
|
|
36270
36545
|
if (description) args.push(`--description=${description}`);
|
|
36271
36546
|
if (location) args.push(`--location=${location}`);
|
|
@@ -36274,10 +36549,28 @@ function registerCalendarTools(server) {
|
|
|
36274
36549
|
if (timezone) args.push(`--timezone=${timezone}`);
|
|
36275
36550
|
if (withZoom) args.push("--with-zoom");
|
|
36276
36551
|
pushReminderFlags(args, { reminders, noReminders });
|
|
36552
|
+
const guests = parseAttendees(attendees);
|
|
36553
|
+
if (guests.length > 0) {
|
|
36554
|
+
const event = { calendarId, summary, from, to, allDay: Boolean(allDay), timezone, location, description, guests, attendees, withZoom: Boolean(withZoom) };
|
|
36555
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
36556
|
+
action: "calendar.create",
|
|
36557
|
+
message: "Review and confirm this event, which will appear on the guests' calendars:",
|
|
36558
|
+
confirmationLabel: CALENDAR_CONFIRM_LABEL,
|
|
36559
|
+
details: event,
|
|
36560
|
+
unsupportedNote: "Create it without attendees instead; the user can add the guests in Google Calendar.",
|
|
36561
|
+
fallback: {
|
|
36562
|
+
tool: "gog_calendar_create",
|
|
36563
|
+
account,
|
|
36564
|
+
confirmToken,
|
|
36565
|
+
subject: () => ({ target: calendarId, payload: event, preview: event })
|
|
36566
|
+
}
|
|
36567
|
+
});
|
|
36568
|
+
if (confirmation) return confirmation;
|
|
36569
|
+
}
|
|
36277
36570
|
return runOrDiagnose(args, { account });
|
|
36278
36571
|
});
|
|
36279
36572
|
server.registerTool("gog_calendar_update", {
|
|
36280
|
-
description: "Update an existing calendar event. Zoom: withZoom adds a Zoom meeting, regenerateZoom replaces the existing one, removeZoom strips it. removeMeet clears the event's Google Meet conference data (e.g. before attaching another provider). Conference flags are independent \u2014 use one per call.",
|
|
36573
|
+
description: "Update an existing calendar event. Zoom: withZoom adds a Zoom meeting, regenerateZoom replaces the existing one, removeZoom strips it. removeMeet clears the event's Google Meet conference data (e.g. before attaching another provider). Conference flags are independent \u2014 use one per call. A change guests can see (time, title, place, description, attendees, attachments, conferencing) on an event that has guests \u2014 or gains them \u2014 reads the event and asks the MCP host to show the user a confirmation prompt with the event as it stands and the change; no invitation email is sent. Reminder-only changes and guest-free events are updated without asking." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36281
36574
|
annotations: { destructiveHint: false },
|
|
36282
36575
|
inputSchema: external_exports.object({
|
|
36283
36576
|
calendarId: external_exports.string().describe("Calendar ID"),
|
|
@@ -36295,9 +36588,10 @@ function registerCalendarTools(server) {
|
|
|
36295
36588
|
removeZoom: external_exports.boolean().optional().describe("Remove the event's Zoom video conference"),
|
|
36296
36589
|
removeMeet: external_exports.boolean().optional().describe("Remove the event's Google Meet video conference (clears conference data only)"),
|
|
36297
36590
|
...reminderParams,
|
|
36298
|
-
account: accountParam
|
|
36591
|
+
account: accountParam,
|
|
36592
|
+
confirmToken: confirmTokenParam
|
|
36299
36593
|
})
|
|
36300
|
-
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
|
|
36594
|
+
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account, confirmToken }, ctx) => {
|
|
36301
36595
|
const args = ["calendar", "update", pos(calendarId), pos(eventId)];
|
|
36302
36596
|
if (summary !== void 0) args.push(`--summary=${summary}`);
|
|
36303
36597
|
if (from !== void 0) args.push(`--from=${from}`);
|
|
@@ -36312,6 +36606,45 @@ function registerCalendarTools(server) {
|
|
|
36312
36606
|
if (removeZoom) args.push("--remove-zoom");
|
|
36313
36607
|
if (removeMeet) args.push("--remove-meet");
|
|
36314
36608
|
pushReminderFlags(args, { reminders, noReminders });
|
|
36609
|
+
const changes = Object.fromEntries(Object.entries({
|
|
36610
|
+
summary,
|
|
36611
|
+
from,
|
|
36612
|
+
to,
|
|
36613
|
+
description,
|
|
36614
|
+
location,
|
|
36615
|
+
attendees,
|
|
36616
|
+
addAttendees,
|
|
36617
|
+
attachments,
|
|
36618
|
+
withZoom,
|
|
36619
|
+
regenerateZoom,
|
|
36620
|
+
removeZoom,
|
|
36621
|
+
removeMeet
|
|
36622
|
+
}).filter(([, v]) => v !== void 0 && v !== false));
|
|
36623
|
+
if (Object.keys(changes).length > 0) {
|
|
36624
|
+
const read = await readEvent(calendarId, eventId, account);
|
|
36625
|
+
if (read.error) return read.error;
|
|
36626
|
+
const { etag, ...current } = read.event;
|
|
36627
|
+
const added = [...parseAttendees(attendees), ...parseAttendees(addAttendees)];
|
|
36628
|
+
if (current.guests.length > 0 || added.length > 0) {
|
|
36629
|
+
const view = { calendarId, eventId, current, changes };
|
|
36630
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
36631
|
+
action: "calendar.update",
|
|
36632
|
+
message: "Review and confirm this change to an event other people are on:",
|
|
36633
|
+
confirmationLabel: CALENDAR_CONFIRM_LABEL,
|
|
36634
|
+
details: view,
|
|
36635
|
+
unsupportedNote: "Ask the user to make this change in Google Calendar.",
|
|
36636
|
+
fallback: {
|
|
36637
|
+
tool: "gog_calendar_update",
|
|
36638
|
+
account,
|
|
36639
|
+
confirmToken,
|
|
36640
|
+
// The etag rotates on any edit, so an event changed elsewhere between
|
|
36641
|
+
// the two phases is DRAFT_CHANGED rather than edited over.
|
|
36642
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
|
|
36643
|
+
}
|
|
36644
|
+
});
|
|
36645
|
+
if (confirmation) return confirmation;
|
|
36646
|
+
}
|
|
36647
|
+
}
|
|
36315
36648
|
return runOrDiagnose(args, { account });
|
|
36316
36649
|
});
|
|
36317
36650
|
server.registerTool("gog_calendar_delete", {
|
|
@@ -36326,21 +36659,40 @@ function registerCalendarTools(server) {
|
|
|
36326
36659
|
return runOrDiagnose(["calendar", "delete", pos(calendarId), pos(eventId), "--force"], { account });
|
|
36327
36660
|
});
|
|
36328
36661
|
server.registerTool("gog_calendar_respond", {
|
|
36329
|
-
description: "Respond to a calendar event invitation.",
|
|
36662
|
+
description: "Respond to a calendar event invitation. The organizer sees the response, so this reads the event and asks the MCP host to show the user a confirmation prompt with the event, organizer, response and comment; nothing is recorded unless they accept." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36330
36663
|
annotations: { destructiveHint: true },
|
|
36331
36664
|
inputSchema: external_exports.object({
|
|
36332
36665
|
calendarId: external_exports.string().describe("Calendar ID"),
|
|
36333
36666
|
eventId: external_exports.string().describe("Event ID"),
|
|
36334
36667
|
status: external_exports.enum(["accepted", "declined", "tentative"]).describe("Response status"),
|
|
36335
36668
|
comment: external_exports.string().optional().describe("Optional comment to include with response"),
|
|
36336
|
-
account: accountParam
|
|
36669
|
+
account: accountParam,
|
|
36670
|
+
confirmToken: confirmTokenParam
|
|
36337
36671
|
})
|
|
36338
|
-
}, async ({ calendarId, eventId, status, comment, account }) => {
|
|
36672
|
+
}, async ({ calendarId, eventId, status, comment, account, confirmToken }, ctx) => {
|
|
36339
36673
|
const args = ["calendar", "respond", pos(calendarId), pos(eventId), `--status=${status}`];
|
|
36340
36674
|
if (comment) args.push(`--comment=${comment}`);
|
|
36675
|
+
const read = await readEvent(calendarId, eventId, account);
|
|
36676
|
+
if (read.error) return read.error;
|
|
36677
|
+
const { etag, ...event } = read.event;
|
|
36678
|
+
const view = { calendarId, eventId, event, response: status, ...comment ? { comment } : {} };
|
|
36679
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
36680
|
+
action: "calendar.respond",
|
|
36681
|
+
message: "Review and confirm this response, which the organizer will see:",
|
|
36682
|
+
confirmationLabel: "Confirm that this response should be recorded now.",
|
|
36683
|
+
details: view,
|
|
36684
|
+
unsupportedNote: "Ask the user to respond from Google Calendar.",
|
|
36685
|
+
fallback: {
|
|
36686
|
+
tool: "gog_calendar_respond",
|
|
36687
|
+
account,
|
|
36688
|
+
confirmToken,
|
|
36689
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
|
|
36690
|
+
}
|
|
36691
|
+
});
|
|
36692
|
+
if (confirmation) return confirmation;
|
|
36341
36693
|
return runOrDiagnose(args, { account });
|
|
36342
36694
|
});
|
|
36343
|
-
registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"' });
|
|
36695
|
+
registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"', vet: vetCalendarRun });
|
|
36344
36696
|
}
|
|
36345
36697
|
|
|
36346
36698
|
// src/attachments.ts
|
|
@@ -36424,6 +36776,23 @@ function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
|
|
|
36424
36776
|
}
|
|
36425
36777
|
|
|
36426
36778
|
// src/tools/chat.ts
|
|
36779
|
+
var CHAT_SEND_WORDS = /* @__PURE__ */ new Set(["send", "create", "post"]);
|
|
36780
|
+
function vetChatRun(subcommand, args) {
|
|
36781
|
+
const sub = subcommand.toLowerCase();
|
|
36782
|
+
if (sub !== "messages" && sub !== "dm") return void 0;
|
|
36783
|
+
const word = hasCommandWord(args, CHAT_SEND_WORDS);
|
|
36784
|
+
if (!word) return void 0;
|
|
36785
|
+
return gatedElsewhere(
|
|
36786
|
+
`gog chat ${sub} ${word.toLowerCase()}`,
|
|
36787
|
+
"gog_chat_run",
|
|
36788
|
+
"posts a message",
|
|
36789
|
+
sub === "dm" ? "gog_chat_dm_send" : "gog_chat_messages_send"
|
|
36790
|
+
);
|
|
36791
|
+
}
|
|
36792
|
+
var CHAT_CONFIRM = {
|
|
36793
|
+
message: "Review and confirm this Google Chat message:",
|
|
36794
|
+
confirmationLabel: "Confirm that this Chat message should be posted now."
|
|
36795
|
+
};
|
|
36427
36796
|
function registerChatTools(server) {
|
|
36428
36797
|
const workspaceOnlyNote = ' WORKSPACE ONLY: Google Chat has no API for consumer accounts, so this fails on an @gmail.com account with "chat requires a Google Workspace account". That is the ACCOUNT, not the token \u2014 re-authorizing or adding scopes will not help.';
|
|
36429
36798
|
const spaceParam = external_exports.string().describe(
|
|
@@ -36529,7 +36898,7 @@ function registerChatTools(server) {
|
|
|
36529
36898
|
return runOrDiagnose(args, { account });
|
|
36530
36899
|
});
|
|
36531
36900
|
server.registerTool("gog_chat_messages_send", {
|
|
36532
|
-
description: "Post a message to a Chat space. THIS IS IMMEDIATELY VISIBLE TO EVERYONE IN THE SPACE and cannot be unsent through this tool, so treat it like sending mail, not like saving a draft. Pass `thread` to reply inside an existing conversation (from gog_chat_threads_list or a message's thread field); omit it to start a new one. Text supports Chat's markdown-ish formatting (*bold*, _italic_, `code`)." + workspaceOnlyNote,
|
|
36901
|
+
description: "Post a message to a Chat space. THIS IS IMMEDIATELY VISIBLE TO EVERYONE IN THE SPACE and cannot be unsent through this tool, so treat it like sending mail, not like saving a draft. Pass `thread` to reply inside an existing conversation (from gog_chat_threads_list or a message's thread field); omit it to start a new one. Text supports Chat's markdown-ish formatting (*bold*, _italic_, `code`). Asks the MCP host to show the user a confirmation prompt with the space, thread, text and attachments first; nothing is posted unless they accept." + CONFIRM_FALLBACK_DESCRIPTION + workspaceOnlyNote,
|
|
36533
36902
|
annotations: { destructiveHint: true },
|
|
36534
36903
|
inputSchema: external_exports.object({
|
|
36535
36904
|
space: spaceParam,
|
|
@@ -36539,9 +36908,10 @@ function registerChatTools(server) {
|
|
|
36539
36908
|
"Attachment file paths, read WHERE GOG RUNS. On a hosted or remote deployment that is not your machine \u2014 use attachInline there instead. Must be inside the server's GOG_FILE_ROOTS directories (default ~/gogcli-mcp-files)."
|
|
36540
36909
|
),
|
|
36541
36910
|
attachInline: attachInlineParam,
|
|
36542
|
-
account: accountParam
|
|
36911
|
+
account: accountParam,
|
|
36912
|
+
confirmToken: confirmTokenParam
|
|
36543
36913
|
})
|
|
36544
|
-
}, async ({ space, text, thread, attach, attachInline, account }) => {
|
|
36914
|
+
}, async ({ space, text, thread, attach, attachInline, account, confirmToken }, ctx) => {
|
|
36545
36915
|
confinePaths(attach, "attach");
|
|
36546
36916
|
if (text === void 0 && !attach?.length && !attachInline?.length) {
|
|
36547
36917
|
throw new Error("A Chat message needs text, an attachment, or both.");
|
|
@@ -36551,20 +36921,56 @@ function registerChatTools(server) {
|
|
|
36551
36921
|
if (thread) args.push(`--thread=${thread}`);
|
|
36552
36922
|
if (attach) for (const path of attach) args.push(`--attach=${path}`);
|
|
36553
36923
|
args.push(...inlineAttachmentArgs("attach", attachInline, args));
|
|
36924
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
36925
|
+
...CHAT_CONFIRM,
|
|
36926
|
+
action: "chat.message-send",
|
|
36927
|
+
details: { space, thread, textPreview: bodyPreview(text), attachments: attachmentNames(attach, attachInline) },
|
|
36928
|
+
fallback: {
|
|
36929
|
+
tool: "gog_chat_messages_send",
|
|
36930
|
+
account,
|
|
36931
|
+
confirmToken,
|
|
36932
|
+
subject: () => {
|
|
36933
|
+
const attachments = attachmentDetails(attach, attachInline);
|
|
36934
|
+
const from = senderPreview(account);
|
|
36935
|
+
return {
|
|
36936
|
+
target: space,
|
|
36937
|
+
payload: { from, space, thread, text, attachments },
|
|
36938
|
+
preview: { from, space, thread, text, attachments: attachmentPreview(attachments) }
|
|
36939
|
+
};
|
|
36940
|
+
}
|
|
36941
|
+
}
|
|
36942
|
+
});
|
|
36943
|
+
if (confirmation) return confirmation;
|
|
36554
36944
|
return runOrDiagnose(args, { account });
|
|
36555
36945
|
});
|
|
36556
36946
|
server.registerTool("gog_chat_dm_send", {
|
|
36557
|
-
description: "Send a direct message to one person by email address, creating the DM space if this is the first message. Delivered immediately and cannot be unsent through this tool. For a room rather than a person, use gog_chat_messages_send." + workspaceOnlyNote,
|
|
36947
|
+
description: "Send a direct message to one person by email address, creating the DM space if this is the first message. Delivered immediately and cannot be unsent through this tool. For a room rather than a person, use gog_chat_messages_send. Asks the MCP host to show the user a confirmation prompt with the recipient and text first; nothing is sent unless they accept." + CONFIRM_FALLBACK_DESCRIPTION + workspaceOnlyNote,
|
|
36558
36948
|
annotations: { destructiveHint: true },
|
|
36559
36949
|
inputSchema: external_exports.object({
|
|
36560
36950
|
email: external_exports.string().describe("Recipient email address"),
|
|
36561
36951
|
text: external_exports.string().describe("Message text"),
|
|
36562
36952
|
thread: threadParam,
|
|
36563
|
-
account: accountParam
|
|
36953
|
+
account: accountParam,
|
|
36954
|
+
confirmToken: confirmTokenParam
|
|
36564
36955
|
})
|
|
36565
|
-
}, async ({ email: email3, text, thread, account }) => {
|
|
36956
|
+
}, async ({ email: email3, text, thread, account, confirmToken }, ctx) => {
|
|
36566
36957
|
const args = ["chat", "dm", "send", pos(email3), `--text=${text}`];
|
|
36567
36958
|
if (thread) args.push(`--thread=${thread}`);
|
|
36959
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
36960
|
+
...CHAT_CONFIRM,
|
|
36961
|
+
action: "chat.dm-send",
|
|
36962
|
+
details: { to: email3, thread, textPreview: bodyPreview(text) },
|
|
36963
|
+
fallback: {
|
|
36964
|
+
tool: "gog_chat_dm_send",
|
|
36965
|
+
account,
|
|
36966
|
+
confirmToken,
|
|
36967
|
+
subject: () => {
|
|
36968
|
+
const from = senderPreview(account);
|
|
36969
|
+
return { target: email3, payload: { from, to: email3, thread, text }, preview: { from, to: email3, thread, text } };
|
|
36970
|
+
}
|
|
36971
|
+
}
|
|
36972
|
+
});
|
|
36973
|
+
if (confirmation) return confirmation;
|
|
36568
36974
|
return runOrDiagnose(args, { account });
|
|
36569
36975
|
});
|
|
36570
36976
|
server.registerTool("gog_chat_dm_space", {
|
|
@@ -36619,11 +37025,45 @@ function registerChatTools(server) {
|
|
|
36619
37025
|
registerRunTool(server, {
|
|
36620
37026
|
service: "chat",
|
|
36621
37027
|
examples: '"spaces", "messages", "dm"',
|
|
37028
|
+
vet: vetChatRun,
|
|
36622
37029
|
note: "Google Chat has no API for consumer accounts: every chat subcommand fails on an @gmail.com account regardless of scopes."
|
|
36623
37030
|
});
|
|
36624
37031
|
}
|
|
36625
37032
|
|
|
36626
37033
|
// src/tools/classroom.ts
|
|
37034
|
+
var CLASSROOM_CREATE_WORDS = /* @__PURE__ */ new Set(["create", "add", "new"]);
|
|
37035
|
+
var CLASSROOM_GATED = {
|
|
37036
|
+
announcements: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
37037
|
+
announcement: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
37038
|
+
ann: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
37039
|
+
invitations: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
|
|
37040
|
+
invitation: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
|
|
37041
|
+
invites: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
|
|
37042
|
+
};
|
|
37043
|
+
function vetClassroomRun(subcommand, args) {
|
|
37044
|
+
const sub = subcommand.toLowerCase();
|
|
37045
|
+
const gated = Object.hasOwn(CLASSROOM_GATED, sub) ? CLASSROOM_GATED[sub] : void 0;
|
|
37046
|
+
if (!gated) return void 0;
|
|
37047
|
+
const word = hasCommandWord(args, CLASSROOM_CREATE_WORDS);
|
|
37048
|
+
return word ? gatedElsewhere(`gog classroom ${sub} ${word.toLowerCase()}`, "gog_classroom_run", gated.does, gated.tool) : void 0;
|
|
37049
|
+
}
|
|
37050
|
+
async function readCourse(courseId, account, runner = runOrDiagnose) {
|
|
37051
|
+
const got = await runner(["classroom", "courses", "get", pos(courseId)], { account });
|
|
37052
|
+
if (got.isError) return { error: got };
|
|
37053
|
+
let course;
|
|
37054
|
+
try {
|
|
37055
|
+
course = JSON.parse(resultText(got))?.course;
|
|
37056
|
+
} catch {
|
|
37057
|
+
course = void 0;
|
|
37058
|
+
}
|
|
37059
|
+
return {
|
|
37060
|
+
course: {
|
|
37061
|
+
id: courseId,
|
|
37062
|
+
...typeof course?.name === "string" ? { name: course.name } : {},
|
|
37063
|
+
...typeof course?.section === "string" ? { section: course.section } : {}
|
|
37064
|
+
}
|
|
37065
|
+
};
|
|
37066
|
+
}
|
|
36627
37067
|
function registerClassroomTools(server) {
|
|
36628
37068
|
server.registerTool("gog_classroom_courses_list", {
|
|
36629
37069
|
description: "List Google Classroom courses.",
|
|
@@ -36906,19 +37346,42 @@ function registerClassroomTools(server) {
|
|
|
36906
37346
|
return runOrDiagnose(["classroom", "announcements", "get", pos(courseId), pos(announcementId)], { account });
|
|
36907
37347
|
});
|
|
36908
37348
|
server.registerTool("gog_classroom_announcements_create", {
|
|
36909
|
-
description: "Create an announcement in a Google Classroom course.",
|
|
37349
|
+
description: "Create an announcement in a Google Classroom course. Unless state is DRAFT (which students cannot see), this reads the course and asks the MCP host to show the user a confirmation prompt with the class, the full text and when it publishes; nothing is posted unless they accept. To stage one without asking, pass state DRAFT." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36910
37350
|
annotations: { destructiveHint: true },
|
|
36911
37351
|
inputSchema: external_exports.object({
|
|
36912
37352
|
courseId: external_exports.string().describe("Course ID"),
|
|
36913
37353
|
text: external_exports.string().describe("Announcement text"),
|
|
36914
|
-
state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State"),
|
|
37354
|
+
state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State (DRAFT is visible only to teachers and needs no confirmation)"),
|
|
36915
37355
|
scheduled: external_exports.string().optional().describe("Scheduled publish time"),
|
|
36916
|
-
account: accountParam
|
|
37356
|
+
account: accountParam,
|
|
37357
|
+
confirmToken: confirmTokenParam
|
|
36917
37358
|
})
|
|
36918
|
-
}, async ({ courseId, text, state, scheduled, account }) => {
|
|
37359
|
+
}, async ({ courseId, text, state, scheduled, account, confirmToken }, ctx) => {
|
|
36919
37360
|
const args = ["classroom", "announcements", "create", pos(courseId), `--text=${text}`];
|
|
36920
37361
|
if (state) args.push(`--state=${state}`);
|
|
36921
37362
|
if (scheduled) args.push(`--scheduled=${scheduled}`);
|
|
37363
|
+
if (state !== "DRAFT") {
|
|
37364
|
+
const read = await readCourse(courseId, account);
|
|
37365
|
+
if (read.error) return read.error;
|
|
37366
|
+
const publishes = scheduled ? `at ${scheduled}` : "immediately";
|
|
37367
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
37368
|
+
action: "classroom.announcement-create",
|
|
37369
|
+
message: "Review and confirm this Classroom announcement:",
|
|
37370
|
+
confirmationLabel: "Confirm that this announcement should be posted to the class.",
|
|
37371
|
+
details: { course: read.course, publishes, textPreview: bodyPreview(text) },
|
|
37372
|
+
unsupportedNote: "Create it with state DRAFT instead; the user can review and post it from Classroom.",
|
|
37373
|
+
fallback: {
|
|
37374
|
+
tool: "gog_classroom_announcements_create",
|
|
37375
|
+
account,
|
|
37376
|
+
confirmToken,
|
|
37377
|
+
subject: () => {
|
|
37378
|
+
const view = { course: read.course, publishes, text };
|
|
37379
|
+
return { target: courseId, payload: view, preview: view };
|
|
37380
|
+
}
|
|
37381
|
+
}
|
|
37382
|
+
});
|
|
37383
|
+
if (confirmation) return confirmation;
|
|
37384
|
+
}
|
|
36922
37385
|
return runOrDiagnose(args, { account });
|
|
36923
37386
|
});
|
|
36924
37387
|
server.registerTool("gog_classroom_topics_list", {
|
|
@@ -37008,6 +37471,7 @@ function registerClassroomTools(server) {
|
|
|
37008
37471
|
registerRunTool(server, {
|
|
37009
37472
|
service: "classroom",
|
|
37010
37473
|
examples: '"guardians", "materials", "guardian-invitations"',
|
|
37474
|
+
vet: vetClassroomRun,
|
|
37011
37475
|
note: "Covers anything not wrapped by the dedicated tools (guardians, guardian-invitations, materials, coursework assignees, announcement assignees, etc.)."
|
|
37012
37476
|
});
|
|
37013
37477
|
}
|
|
@@ -37168,7 +37632,18 @@ function registerDocsTools(server) {
|
|
|
37168
37632
|
}
|
|
37169
37633
|
|
|
37170
37634
|
// src/tools/drive.ts
|
|
37635
|
+
function vetDriveRun(subcommand, _args) {
|
|
37636
|
+
return subcommand.toLowerCase() === "share" ? gatedElsewhere("gog drive share", "gog_drive_run", "grants access to a file", "gog_drive_share") : void 0;
|
|
37637
|
+
}
|
|
37171
37638
|
var GOOGLE_DOC_MIME = "application/vnd.google-apps.document";
|
|
37639
|
+
function shareTargetMeta(raw) {
|
|
37640
|
+
try {
|
|
37641
|
+
const { name, mimeType } = fileMeta(raw);
|
|
37642
|
+
return { name, mimeType };
|
|
37643
|
+
} catch {
|
|
37644
|
+
return {};
|
|
37645
|
+
}
|
|
37646
|
+
}
|
|
37172
37647
|
function fileMeta(raw) {
|
|
37173
37648
|
const parsed = JSON.parse(raw);
|
|
37174
37649
|
const f = parsed.file ?? parsed;
|
|
@@ -37291,7 +37766,7 @@ function registerDriveTools(server) {
|
|
|
37291
37766
|
return runOrDiagnose(args, { account });
|
|
37292
37767
|
});
|
|
37293
37768
|
server.registerTool("gog_drive_share", {
|
|
37294
|
-
description: "Share a Google Drive file or folder.",
|
|
37769
|
+
description: "Share a Google Drive file or folder. Granting access is the risk even though no email is sent (gog does not notify by default), so this reads the file and asks the MCP host to show the user a confirmation prompt naming it, who gets access and with what role; nothing is shared unless they accept." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
37295
37770
|
annotations: { destructiveHint: true },
|
|
37296
37771
|
inputSchema: external_exports.object({
|
|
37297
37772
|
fileId: external_exports.string().describe("File or folder ID"),
|
|
@@ -37299,9 +37774,35 @@ function registerDriveTools(server) {
|
|
|
37299
37774
|
email: external_exports.string().optional().describe("User email (required when to=user)"),
|
|
37300
37775
|
domain: external_exports.string().optional().describe("Domain (required when to=domain)"),
|
|
37301
37776
|
role: external_exports.enum(["reader", "writer"]).optional().describe("Permission role (default: reader)"),
|
|
37302
|
-
account: accountParam
|
|
37777
|
+
account: accountParam,
|
|
37778
|
+
confirmToken: confirmTokenParam
|
|
37303
37779
|
})
|
|
37304
|
-
}, async ({ fileId, to, email: email3, domain: domain2, role, account }) => {
|
|
37780
|
+
}, async ({ fileId, to, email: email3, domain: domain2, role, account, confirmToken }, ctx) => {
|
|
37781
|
+
const got = await runOrDiagnose(["drive", "get", pos(fileId)], { account });
|
|
37782
|
+
if (got.isError) return got;
|
|
37783
|
+
const file2 = { id: fileId, ...shareTargetMeta(resultText(got)) };
|
|
37784
|
+
const grant = {
|
|
37785
|
+
file: file2,
|
|
37786
|
+
to,
|
|
37787
|
+
...email3 ? { email: email3 } : {},
|
|
37788
|
+
...domain2 ? { domain: domain2 } : {},
|
|
37789
|
+
role: role ?? "reader",
|
|
37790
|
+
publicLink: to === "anyone"
|
|
37791
|
+
};
|
|
37792
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
37793
|
+
action: "drive.share",
|
|
37794
|
+
message: to === "anyone" ? "Review and confirm making this file available to ANYONE with the link:" : "Review and confirm this Drive share:",
|
|
37795
|
+
confirmationLabel: "Confirm that this access should be granted now.",
|
|
37796
|
+
details: grant,
|
|
37797
|
+
unsupportedNote: "Ask the user to share it themselves from Google Drive.",
|
|
37798
|
+
fallback: {
|
|
37799
|
+
tool: "gog_drive_share",
|
|
37800
|
+
account,
|
|
37801
|
+
confirmToken,
|
|
37802
|
+
subject: () => ({ target: fileId, payload: grant, preview: grant })
|
|
37803
|
+
}
|
|
37804
|
+
});
|
|
37805
|
+
if (confirmation) return confirmation;
|
|
37305
37806
|
const args = ["drive", "share", pos(fileId), `--to=${to}`];
|
|
37306
37807
|
if (email3) args.push(`--email=${email3}`);
|
|
37307
37808
|
if (domain2) args.push(`--domain=${domain2}`);
|
|
@@ -37414,7 +37915,7 @@ function registerDriveTools(server) {
|
|
|
37414
37915
|
return diagnose(err);
|
|
37415
37916
|
}
|
|
37416
37917
|
});
|
|
37417
|
-
registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"' });
|
|
37918
|
+
registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"', vet: vetDriveRun });
|
|
37418
37919
|
}
|
|
37419
37920
|
|
|
37420
37921
|
// src/gmail-results.ts
|
|
@@ -37540,24 +38041,16 @@ var STAGING_TWIN = {
|
|
|
37540
38041
|
var UNSUPPORTED_NOTE2 = {
|
|
37541
38042
|
"gmail.drafts-send": "The draft is still saved: ask the user to review it and send it from Gmail."
|
|
37542
38043
|
};
|
|
37543
|
-
|
|
37544
|
-
function bodyPreview(text) {
|
|
37545
|
-
if (!text) return void 0;
|
|
37546
|
-
if (text.length <= BODY_PREVIEW_MAX) return text;
|
|
37547
|
-
return `${text.slice(0, BODY_PREVIEW_MAX)}\u2026 [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
|
|
37548
|
-
}
|
|
37549
|
-
function attachmentNames(paths, inline) {
|
|
37550
|
-
return [...paths ?? [], ...(inline ?? []).map((a) => a.filename)];
|
|
37551
|
-
}
|
|
37552
|
-
function requireGmailDispatchConfirmation(ctx, op, details) {
|
|
38044
|
+
async function requireGmailDispatchConfirmation(ctx, op, details, fallback) {
|
|
37553
38045
|
const twin = STAGING_TWIN[op];
|
|
37554
38046
|
const note = twin ? `Stage it with ${twin} instead; the user can review the draft and send it from Gmail (gog_gmail_drafts_send also asks for confirmation).` : UNSUPPORTED_NOTE2[op];
|
|
37555
|
-
return
|
|
38047
|
+
return requireDispatchConfirmation(ctx, {
|
|
37556
38048
|
action: op,
|
|
37557
38049
|
message: op === "gmail.filter-forward" ? "Review and confirm this mail-forwarding filter:" : "Review and confirm this email dispatch:",
|
|
37558
38050
|
details,
|
|
37559
38051
|
confirmationLabel: op === "gmail.filter-forward" ? "Confirm that matching mail should be forwarded automatically from now on." : "Confirm that this email should be sent now.",
|
|
37560
|
-
|
|
38052
|
+
unsupportedNote: note,
|
|
38053
|
+
...fallback ? { fallback: { instruction: CONFIRM_SEND_INSTRUCTION, ...fallback } } : {}
|
|
37561
38054
|
});
|
|
37562
38055
|
}
|
|
37563
38056
|
var EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/gi;
|
|
@@ -37611,10 +38104,6 @@ function logGmailDispatch(tool, recipients, account) {
|
|
|
37611
38104
|
process.stderr.write(`${JSON.stringify(event)}
|
|
37612
38105
|
`);
|
|
37613
38106
|
}
|
|
37614
|
-
function resultText(result) {
|
|
37615
|
-
const first = result.content[0];
|
|
37616
|
-
return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
|
|
37617
|
-
}
|
|
37618
38107
|
|
|
37619
38108
|
// src/tools/gmail.ts
|
|
37620
38109
|
var replySchema = {
|
|
@@ -37662,7 +38151,7 @@ function appendReplyFlags(args, f) {
|
|
|
37662
38151
|
if (f.signatureFile) args.push(`--signature-file=${f.signatureFile}`);
|
|
37663
38152
|
args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
|
|
37664
38153
|
}
|
|
37665
|
-
var sendReplySchema = external_exports.object(replySchema);
|
|
38154
|
+
var sendReplySchema = external_exports.object({ ...replySchema, confirmToken: confirmTokenParam });
|
|
37666
38155
|
function parseMetadataHeaders(raw) {
|
|
37667
38156
|
let parsed;
|
|
37668
38157
|
try {
|
|
@@ -37687,12 +38176,69 @@ function computeReplyRecipients(kind, headers, flags) {
|
|
|
37687
38176
|
}
|
|
37688
38177
|
return emails;
|
|
37689
38178
|
}
|
|
37690
|
-
|
|
38179
|
+
function metadataThreadId(raw) {
|
|
38180
|
+
try {
|
|
38181
|
+
const threadId = JSON.parse(raw)?.message?.threadId;
|
|
38182
|
+
return typeof threadId === "string" ? threadId : void 0;
|
|
38183
|
+
} catch {
|
|
38184
|
+
return void 0;
|
|
38185
|
+
}
|
|
38186
|
+
}
|
|
38187
|
+
function replyTokenSubject(kind, messageId, account, headers, threadId, flags) {
|
|
38188
|
+
const removed = new Set(extractEmails(...flags.remove ?? []));
|
|
38189
|
+
const keep = (emails) => emails.filter((e) => !removed.has(e));
|
|
38190
|
+
const to = keep(extractEmails(headers.from, ...kind === "reply-all" ? [headers.to] : [], ...flags.to ?? []));
|
|
38191
|
+
const cc = keep(extractEmails(...kind === "reply-all" ? [headers.cc] : [], ...flags.cc ?? []));
|
|
38192
|
+
const bcc = keep(extractEmails(...flags.bcc ?? []));
|
|
38193
|
+
const attachments = attachmentDetails(flags.attach, flags.attachInline);
|
|
38194
|
+
const subject = flags.subject || (headers.subject ? `Re: ${headers.subject}` : void 0);
|
|
38195
|
+
const from = flags.from ?? (flags.autoFromAddressedAlias ? "the send-as alias the original was addressed to" : senderPreview(account));
|
|
38196
|
+
const localFiles = attachmentDetails([flags.bodyHtmlFile, flags.signatureFile].filter((p) => Boolean(p)), void 0);
|
|
38197
|
+
return {
|
|
38198
|
+
target: messageId,
|
|
38199
|
+
payload: {
|
|
38200
|
+
kind,
|
|
38201
|
+
to,
|
|
38202
|
+
cc,
|
|
38203
|
+
bcc,
|
|
38204
|
+
subject,
|
|
38205
|
+
from,
|
|
38206
|
+
body: flags.body,
|
|
38207
|
+
bodyHtml: flags.bodyHtml,
|
|
38208
|
+
localFiles,
|
|
38209
|
+
signature: Boolean(flags.signature),
|
|
38210
|
+
signatureFrom: flags.signatureFrom,
|
|
38211
|
+
attachments,
|
|
38212
|
+
quote: !flags.noQuote,
|
|
38213
|
+
threadId,
|
|
38214
|
+
inReplyTo: headers.message_id,
|
|
38215
|
+
references: headers.references
|
|
38216
|
+
},
|
|
38217
|
+
preview: {
|
|
38218
|
+
from,
|
|
38219
|
+
to,
|
|
38220
|
+
cc,
|
|
38221
|
+
bcc,
|
|
38222
|
+
subject,
|
|
38223
|
+
body: flags.body,
|
|
38224
|
+
...flags.bodyHtml ? { bodyHtml: flags.bodyHtml } : {},
|
|
38225
|
+
...flags.bodyHtmlFile ? { bodyHtmlFile: flags.bodyHtmlFile } : {},
|
|
38226
|
+
...flags.signature || flags.signatureFrom || flags.signatureFile ? { signature: flags.signatureFile ?? flags.signatureFrom ?? "the Gmail signature of the sending address" } : {},
|
|
38227
|
+
quotesOriginal: !flags.noQuote,
|
|
38228
|
+
attachments: attachmentPreview(attachments),
|
|
38229
|
+
replyingToMessageId: messageId,
|
|
38230
|
+
threadId,
|
|
38231
|
+
inReplyTo: headers.message_id
|
|
38232
|
+
}
|
|
38233
|
+
};
|
|
38234
|
+
}
|
|
38235
|
+
async function sendReply(kind, toolName, messageId, account, flags, ctx, confirmToken) {
|
|
37691
38236
|
const metaResult = await runOrDiagnose(["gmail", "get", pos(messageId), "--format=metadata"], { account });
|
|
37692
38237
|
if (metaResult.isError) return metaResult;
|
|
37693
|
-
const
|
|
38238
|
+
const metaText = resultText(metaResult);
|
|
38239
|
+
const headers = parseMetadataHeaders(metaText);
|
|
37694
38240
|
const recipients = computeReplyRecipients(kind, headers, flags);
|
|
37695
|
-
const confirmation = requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
|
|
38241
|
+
const confirmation = await requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
|
|
37696
38242
|
messageId,
|
|
37697
38243
|
recipients,
|
|
37698
38244
|
recipientCount: recipients.length,
|
|
@@ -37703,6 +38249,13 @@ async function sendReply(kind, toolName, messageId, account, flags, ctx) {
|
|
|
37703
38249
|
bodyHtmlFile: flags.bodyHtmlFile,
|
|
37704
38250
|
attachmentCount: (flags.attach?.length ?? 0) + (flags.attachInline?.length ?? 0),
|
|
37705
38251
|
attachments: attachmentNames(flags.attach, flags.attachInline)
|
|
38252
|
+
}, {
|
|
38253
|
+
tool: toolName,
|
|
38254
|
+
account,
|
|
38255
|
+
confirmToken,
|
|
38256
|
+
// The metadata read above IS the phase-2 re-read: it runs on every call,
|
|
38257
|
+
// so the original's Message-ID/References are fresh here.
|
|
38258
|
+
subject: () => replyTokenSubject(kind, messageId, account, headers, metadataThreadId(metaText), flags)
|
|
37706
38259
|
});
|
|
37707
38260
|
if (confirmation) return confirmation;
|
|
37708
38261
|
const args = ["gmail", kind, pos(messageId)];
|
|
@@ -37720,7 +38273,7 @@ function vetGmailRun(subcommand, args) {
|
|
|
37720
38273
|
return `gog gmail ${subcommand} can forward or hand over mail and is not available through gog_gmail_run. Use the dedicated gog_gmail_* tool instead.`;
|
|
37721
38274
|
}
|
|
37722
38275
|
if (subcommand === "settings") {
|
|
37723
|
-
const blocked = args
|
|
38276
|
+
const blocked = hasCommandWord(args, GMAIL_RUN_BLOCKED_SETTINGS);
|
|
37724
38277
|
if (blocked) {
|
|
37725
38278
|
return `gog gmail settings ${blocked} can forward or hand over mail and is not available through gog_gmail_run. Use the dedicated gog_gmail_* tool instead.`;
|
|
37726
38279
|
}
|
|
@@ -37779,7 +38332,7 @@ function registerGmailTools(server) {
|
|
|
37779
38332
|
return runOrDiagnose(args, { account });
|
|
37780
38333
|
});
|
|
37781
38334
|
server.registerTool("gog_gmail_send", {
|
|
37782
|
-
description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the recipients, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. Two ways to attach a file: `attach` takes paths READ ON THE GOG SERVER, and `attachInline` takes the bytes themselves. Use attachInline unless you know the file exists on the same machine gog runs on \u2014 on the hosted connector and any remote deployment there is no shared filesystem, so no path you can name resolves there and `attach` will fail with "no such file or directory". When either is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were embedded. NOT the tool for answering a message: replyToMessageId only files this in the right thread \u2014 the subject, recipients and body are entirely yours, and the original is not quoted unless you set quote. Use gog_gmail_reply / gog_gmail_reply_all instead, which inherit all three.',
|
|
38335
|
+
description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the recipients, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. Two ways to attach a file: `attach` takes paths READ ON THE GOG SERVER, and `attachInline` takes the bytes themselves. Use attachInline unless you know the file exists on the same machine gog runs on \u2014 on the hosted connector and any remote deployment there is no shared filesystem, so no path you can name resolves there and `attach` will fail with "no such file or directory". When either is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were embedded. NOT the tool for answering a message: replyToMessageId only files this in the right thread \u2014 the subject, recipients and body are entirely yours, and the original is not quoted unless you set quote. Use gog_gmail_reply / gog_gmail_reply_all instead, which inherit all three.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
37783
38336
|
annotations: { destructiveHint: true },
|
|
37784
38337
|
inputSchema: external_exports.object({
|
|
37785
38338
|
to: external_exports.string().describe("Recipient(s), comma-separated"),
|
|
@@ -37792,9 +38345,10 @@ function registerGmailTools(server) {
|
|
|
37792
38345
|
quote: external_exports.boolean().optional().describe("Include the original message quoted below the body. Requires replyToMessageId or threadId. gog quotes by DEFAULT on gmail reply but never on gmail send, so without this a threaded send arrives with the original nowhere in it."),
|
|
37793
38346
|
attach: external_exports.array(external_exports.string()).optional().describe(`File paths to attach (repeatable), resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on a hosted deployment (e.g. mcp-host) these paths do not exist and the call fails with "no such file or directory" \u2014 use attachInline there. Each file is read on the server, base64-encoded with a MIME type inferred from its extension, and added as a multipart attachment. Must be inside the server's GOG_FILE_ROOTS directories (default ~/gogcli-mcp-files).`),
|
|
37794
38347
|
attachInline: attachInlineParam,
|
|
37795
|
-
account: accountParam
|
|
38348
|
+
account: accountParam,
|
|
38349
|
+
confirmToken: confirmTokenParam
|
|
37796
38350
|
})
|
|
37797
|
-
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account }, ctx) => {
|
|
38351
|
+
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account, confirmToken }, ctx) => {
|
|
37798
38352
|
confinePaths(attach, "attach");
|
|
37799
38353
|
const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
|
|
37800
38354
|
if (cc) args.push(`--cc=${cc}`);
|
|
@@ -37806,7 +38360,7 @@ function registerGmailTools(server) {
|
|
|
37806
38360
|
const inline = inlineAttachmentArgs("attach", attachInline, args);
|
|
37807
38361
|
args.push(...inline);
|
|
37808
38362
|
const recipients = extractEmails(to, cc, bcc);
|
|
37809
|
-
const confirmation = requireGmailDispatchConfirmation(ctx, "gmail.send", {
|
|
38363
|
+
const confirmation = await requireGmailDispatchConfirmation(ctx, "gmail.send", {
|
|
37810
38364
|
to,
|
|
37811
38365
|
cc,
|
|
37812
38366
|
bcc,
|
|
@@ -37819,6 +38373,32 @@ function registerGmailTools(server) {
|
|
|
37819
38373
|
quoting: Boolean(quote),
|
|
37820
38374
|
attachmentCount: (attach?.length ?? 0) + (attachInline?.length ?? 0),
|
|
37821
38375
|
attachments: attachmentNames(attach, attachInline)
|
|
38376
|
+
}, {
|
|
38377
|
+
tool: "gog_gmail_send",
|
|
38378
|
+
account,
|
|
38379
|
+
confirmToken,
|
|
38380
|
+
// Nothing is stored between the phases: the payload IS the arguments,
|
|
38381
|
+
// so phase 2 must repeat them and any difference is a changed send.
|
|
38382
|
+
subject: () => {
|
|
38383
|
+
const attachments = attachmentDetails(attach, attachInline);
|
|
38384
|
+
const from = senderPreview(account);
|
|
38385
|
+
return {
|
|
38386
|
+
target: replyToMessageId ?? threadId ?? "",
|
|
38387
|
+
payload: { from, to, cc, bcc, subject, body, attachments, threadId, inReplyTo: replyToMessageId, quote: Boolean(quote) },
|
|
38388
|
+
preview: {
|
|
38389
|
+
from,
|
|
38390
|
+
to,
|
|
38391
|
+
cc,
|
|
38392
|
+
bcc,
|
|
38393
|
+
subject,
|
|
38394
|
+
body,
|
|
38395
|
+
attachments: attachmentPreview(attachments),
|
|
38396
|
+
threadId,
|
|
38397
|
+
inReplyTo: replyToMessageId,
|
|
38398
|
+
quotesOriginal: Boolean(quote)
|
|
38399
|
+
}
|
|
38400
|
+
};
|
|
38401
|
+
}
|
|
37822
38402
|
});
|
|
37823
38403
|
if (confirmation) return confirmation;
|
|
37824
38404
|
const result = await runOrDiagnose(args, { account });
|
|
@@ -37826,22 +38406,22 @@ function registerGmailTools(server) {
|
|
|
37826
38406
|
return result;
|
|
37827
38407
|
});
|
|
37828
38408
|
server.registerTool("gog_gmail_reply", {
|
|
37829
|
-
description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipient, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. To STAGE a reply instead of sending it, use gog_gmail_drafts_reply (gogcli-mcp-gmail only), which never needs confirmation. Reply to a Gmail message (goes to the original sender only). USE THIS, not gog_gmail_send, whenever you are answering a message: it threads off the original AND inherits its "Re:" subject and quotes its body below yours, which gog_gmail_send does not \u2014 a send with replyToMessageId lands in the right thread but reads as a brand-new message, with the original nowhere in it. To answer every participant use gog_gmail_reply_all. The gogcli-mcp-gmail package adds two more routes with the same composition: gog_gmail_autoreply to reply across every message matching a query, and gog_gmail_drafts_reply to stage this exact reply as a draft instead of sending it.',
|
|
38409
|
+
description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipient, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. To STAGE a reply instead of sending it, use gog_gmail_drafts_reply (gogcli-mcp-gmail only), which never needs confirmation. Reply to a Gmail message (goes to the original sender only). USE THIS, not gog_gmail_send, whenever you are answering a message: it threads off the original AND inherits its "Re:" subject and quotes its body below yours, which gog_gmail_send does not \u2014 a send with replyToMessageId lands in the right thread but reads as a brand-new message, with the original nowhere in it. To answer every participant use gog_gmail_reply_all. The gogcli-mcp-gmail package adds two more routes with the same composition: gog_gmail_autoreply to reply across every message matching a query, and gog_gmail_drafts_reply to stage this exact reply as a draft instead of sending it.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
37830
38410
|
annotations: { destructiveHint: true },
|
|
37831
38411
|
inputSchema: sendReplySchema
|
|
37832
|
-
}, async ({ messageId, account, ...flags }, ctx) => {
|
|
38412
|
+
}, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
|
|
37833
38413
|
assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
|
|
37834
38414
|
confineReplyPaths(flags);
|
|
37835
|
-
return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx);
|
|
38415
|
+
return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx, confirmToken);
|
|
37836
38416
|
});
|
|
37837
38417
|
server.registerTool("gog_gmail_reply_all", {
|
|
37838
|
-
description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipients, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. To STAGE a reply-all instead of sending it, use gog_gmail_drafts_reply_all (gogcli-mcp-gmail only), which never needs confirmation. Reply to all participants of a Gmail message (the sender plus every To/Cc recipient). Same inherited "Re:" subject and quoted original as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all.',
|
|
38418
|
+
description: 'SENDS MAIL \u2014 asks the MCP host to show a confirmation prompt with the resolved recipients, subject, a preview of the body and the attachment names. Mail is sent only after the user accepts that prompt. To STAGE a reply-all instead of sending it, use gog_gmail_drafts_reply_all (gogcli-mcp-gmail only), which never needs confirmation. Reply to all participants of a Gmail message (the sender plus every To/Cc recipient). Same inherited "Re:" subject and quoted original as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
37839
38419
|
annotations: { destructiveHint: true },
|
|
37840
38420
|
inputSchema: sendReplySchema
|
|
37841
|
-
}, async ({ messageId, account, ...flags }, ctx) => {
|
|
38421
|
+
}, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
|
|
37842
38422
|
assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
|
|
37843
38423
|
confineReplyPaths(flags);
|
|
37844
|
-
return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx);
|
|
38424
|
+
return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx, confirmToken);
|
|
37845
38425
|
});
|
|
37846
38426
|
registerRunTool(server, {
|
|
37847
38427
|
service: "gmail",
|
|
@@ -38182,7 +38762,7 @@ function registerTasksTools(server) {
|
|
|
38182
38762
|
}
|
|
38183
38763
|
|
|
38184
38764
|
// src/server.ts
|
|
38185
|
-
var VERSION = true ? "4.
|
|
38765
|
+
var VERSION = true ? "4.4.0" : "0.0.0";
|
|
38186
38766
|
var BASE_TOOL_REGISTRARS = [
|
|
38187
38767
|
registerApiTools,
|
|
38188
38768
|
registerAppScriptTools,
|
|
@@ -38234,12 +38814,12 @@ async function bootstrapGogAuth(env = process.env, options = {}) {
|
|
|
38234
38814
|
const [clientId, clientSecret, refreshToken, account] = values;
|
|
38235
38815
|
const { spawner } = options;
|
|
38236
38816
|
try {
|
|
38237
|
-
const { createHash:
|
|
38817
|
+
const { createHash: createHash4 } = await import("node:crypto");
|
|
38238
38818
|
const { mkdir, readFile, writeFile, chmod } = await import("node:fs/promises");
|
|
38239
38819
|
const home = options.home ?? (await import("node:os")).homedir();
|
|
38240
38820
|
const dir = join5(home, ".gogcli-mcp");
|
|
38241
38821
|
const marker = join5(dir, AUTH_BOOTSTRAP_MARKER);
|
|
38242
|
-
const fingerprint =
|
|
38822
|
+
const fingerprint = createHash4("sha256").update(values.join("\0")).digest("hex");
|
|
38243
38823
|
const previous = await readFile(marker, "utf8").catch(() => void 0);
|
|
38244
38824
|
if (previous === fingerprint && await accountListed(account, spawner)) return "present";
|
|
38245
38825
|
await run(
|