gogcli-mcp 4.3.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +784 -112
- package/dist/lib.js +757 -78
- package/manifest.json +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/dispatch-confirmation.ts +204 -0
- package/src/gmail-dispatch-guard.ts +36 -36
- package/src/lib.ts +9 -0
- package/src/send-confirm-token.ts +29 -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 +246 -14
- package/tests/send-confirm-token.test.ts +20 -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 +406 -0
- package/tests/tools/drive.test.ts +4 -1
- package/tests/tools/gmail-confirm-token.test.ts +276 -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;
|
|
@@ -34554,10 +34554,8 @@ function errorResult(message) {
|
|
|
34554
34554
|
|
|
34555
34555
|
// ../../node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
|
|
34556
34556
|
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
34557
|
-
|
|
34558
|
-
|
|
34559
|
-
var UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
|
|
34560
|
-
var STATE_PREFIX = "mcpu.confirm.v1.";
|
|
34557
|
+
|
|
34558
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/server/canonical.js
|
|
34561
34559
|
function canonicalJson(value) {
|
|
34562
34560
|
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
34563
34561
|
return JSON.stringify(value);
|
|
@@ -34568,7 +34566,7 @@ function canonicalJson(value) {
|
|
|
34568
34566
|
if (typeof value === "bigint")
|
|
34569
34567
|
return `{"$bigint":"${value.toString()}"}`;
|
|
34570
34568
|
if (typeof value === "function" || typeof value === "symbol") {
|
|
34571
|
-
throw new TypeError(`
|
|
34569
|
+
throw new TypeError(`confirmation: cannot bind a ${typeof value} argument value.`);
|
|
34572
34570
|
}
|
|
34573
34571
|
if (Array.isArray(value))
|
|
34574
34572
|
return `[${value.map(canonicalJson).join(",")}]`;
|
|
@@ -34590,7 +34588,7 @@ function canonicalJson(value) {
|
|
|
34590
34588
|
}
|
|
34591
34589
|
const proto = Object.getPrototypeOf(value);
|
|
34592
34590
|
if (proto !== Object.prototype && proto !== null) {
|
|
34593
|
-
throw new TypeError("
|
|
34591
|
+
throw new TypeError("confirmation: cannot bind a non-plain object argument value (class instance).");
|
|
34594
34592
|
}
|
|
34595
34593
|
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
34596
34594
|
return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
|
|
@@ -34598,6 +34596,12 @@ function canonicalJson(value) {
|
|
|
34598
34596
|
function escapeKey(key) {
|
|
34599
34597
|
return key.startsWith("$") ? `$${key}` : key;
|
|
34600
34598
|
}
|
|
34599
|
+
|
|
34600
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
|
|
34601
|
+
var DEFAULT_REQUEST_KEY = "confirmation";
|
|
34602
|
+
var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
|
|
34603
|
+
var UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
|
|
34604
|
+
var STATE_PREFIX = "mcpu.confirm.v1.";
|
|
34601
34605
|
function bindingKey(binding) {
|
|
34602
34606
|
if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
|
|
34603
34607
|
throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
|
|
@@ -34700,6 +34704,264 @@ ${JSON.stringify(preview, null, 2)}`,
|
|
|
34700
34704
|
});
|
|
34701
34705
|
}
|
|
34702
34706
|
|
|
34707
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js
|
|
34708
|
+
import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
34709
|
+
function createSpentTokenStore() {
|
|
34710
|
+
const spent2 = /* @__PURE__ */ new Map();
|
|
34711
|
+
return {
|
|
34712
|
+
has: (nonce) => spent2.has(nonce),
|
|
34713
|
+
add: (nonce, exp) => {
|
|
34714
|
+
spent2.set(nonce, exp);
|
|
34715
|
+
},
|
|
34716
|
+
prune: (now) => {
|
|
34717
|
+
for (const [nonce, exp] of spent2)
|
|
34718
|
+
if (exp < now)
|
|
34719
|
+
spent2.delete(nonce);
|
|
34720
|
+
},
|
|
34721
|
+
clear: () => spent2.clear(),
|
|
34722
|
+
get size() {
|
|
34723
|
+
return spent2.size;
|
|
34724
|
+
}
|
|
34725
|
+
};
|
|
34726
|
+
}
|
|
34727
|
+
var PROCESS_SPENT = createSpentTokenStore();
|
|
34728
|
+
var PREFIX = "mcpu.token.v1.";
|
|
34729
|
+
var DEFAULT_TTL_SECONDS = 600;
|
|
34730
|
+
function tokenKey(key) {
|
|
34731
|
+
const bytes = typeof key === "string" ? Buffer.from(key, "utf8") : Buffer.from(key);
|
|
34732
|
+
if (bytes.length < 32)
|
|
34733
|
+
throw new RangeError("confirm token: key must be at least 32 bytes.");
|
|
34734
|
+
return bytes;
|
|
34735
|
+
}
|
|
34736
|
+
function ttlOf(ttlSeconds) {
|
|
34737
|
+
if (ttlSeconds === void 0)
|
|
34738
|
+
return DEFAULT_TTL_SECONDS;
|
|
34739
|
+
if (!(Number.isFinite(ttlSeconds) && ttlSeconds > 0)) {
|
|
34740
|
+
throw new RangeError("confirm token: ttlSeconds must be a finite number greater than 0.");
|
|
34741
|
+
}
|
|
34742
|
+
return ttlSeconds;
|
|
34743
|
+
}
|
|
34744
|
+
function sign(key, body) {
|
|
34745
|
+
return createHmac2("sha256", key).update(`${PREFIX}${body}`).digest();
|
|
34746
|
+
}
|
|
34747
|
+
function hashConfirmPayload(payload) {
|
|
34748
|
+
return createHash2("sha256").update(canonicalJson(payload)).digest("base64url");
|
|
34749
|
+
}
|
|
34750
|
+
function issueConfirmToken(key, binding, options = {}) {
|
|
34751
|
+
const k = tokenKey(key);
|
|
34752
|
+
const now = options.now ?? Date.now();
|
|
34753
|
+
const exp = now + ttlOf(options.ttlSeconds) * 1e3;
|
|
34754
|
+
const claims = {
|
|
34755
|
+
t: binding.tool,
|
|
34756
|
+
...binding.account === void 0 ? {} : { a: binding.account },
|
|
34757
|
+
g: binding.target,
|
|
34758
|
+
...binding.revision === void 0 ? {} : { r: binding.revision },
|
|
34759
|
+
h: binding.payloadHash,
|
|
34760
|
+
exp,
|
|
34761
|
+
n: randomBytes(16).toString("base64url")
|
|
34762
|
+
};
|
|
34763
|
+
const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
|
|
34764
|
+
return { token: `${PREFIX}${body}.${sign(k, body).toString("base64url")}`, expiresAt: new Date(exp).toISOString() };
|
|
34765
|
+
}
|
|
34766
|
+
function parseClaims(body) {
|
|
34767
|
+
try {
|
|
34768
|
+
const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
34769
|
+
return claims && typeof claims === "object" ? claims : void 0;
|
|
34770
|
+
} catch {
|
|
34771
|
+
return void 0;
|
|
34772
|
+
}
|
|
34773
|
+
}
|
|
34774
|
+
function verifyConfirmToken(key, token, binding, options = {}) {
|
|
34775
|
+
const k = tokenKey(key);
|
|
34776
|
+
const now = options.now ?? Date.now();
|
|
34777
|
+
const spent2 = options.spent ?? PROCESS_SPENT;
|
|
34778
|
+
spent2.prune(now);
|
|
34779
|
+
if (!token.startsWith(PREFIX))
|
|
34780
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
34781
|
+
const rest = token.slice(PREFIX.length);
|
|
34782
|
+
const dot = rest.indexOf(".");
|
|
34783
|
+
const body = dot < 0 ? "" : rest.slice(0, dot);
|
|
34784
|
+
const mac3 = Buffer.from(dot < 0 ? "" : rest.slice(dot + 1), "base64url");
|
|
34785
|
+
const expected = sign(k, body);
|
|
34786
|
+
if (!body || mac3.length !== expected.length || !timingSafeEqual2(mac3, expected))
|
|
34787
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
34788
|
+
const claims = parseClaims(body);
|
|
34789
|
+
if (!claims)
|
|
34790
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
34791
|
+
if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
|
|
34792
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
34793
|
+
}
|
|
34794
|
+
if (spent2.has(claims.n))
|
|
34795
|
+
return { ok: false, error: "TOKEN_REUSED" };
|
|
34796
|
+
if (now > claims.exp)
|
|
34797
|
+
return { ok: false, error: "TOKEN_EXPIRED" };
|
|
34798
|
+
if (claims.r !== binding.revision)
|
|
34799
|
+
return { ok: false, error: "DRAFT_CHANGED", reason: "revision-changed" };
|
|
34800
|
+
if (claims.h !== binding.payloadHash)
|
|
34801
|
+
return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
|
|
34802
|
+
spent2.add(claims.n, claims.exp);
|
|
34803
|
+
return { ok: true };
|
|
34804
|
+
}
|
|
34805
|
+
var CONFIRM_TOKEN_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
|
|
34806
|
+
var confirmTokenParam = external_exports.string().optional().describe(`ONLY for the two-step confirmation fallback (a client without MCP elicitation). The confirmToken from this same tool's phase-1 "confirmation-required" response, passed back ONLY after the user has seen that preview and explicitly approved it in chat \u2014 never on the first call, never invented, never reused. Call again with the same arguments. Ignored when the client supports elicitation.`);
|
|
34807
|
+
var TOKEN_ERROR_NOTE = {
|
|
34808
|
+
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.",
|
|
34809
|
+
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.",
|
|
34810
|
+
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."
|
|
34811
|
+
};
|
|
34812
|
+
var CHANGED_NOTE = {
|
|
34813
|
+
"revision-changed": "Nothing was sent or changed: the target was edited since the user approved it (its version rotated), so what would happen is not what they saw.",
|
|
34814
|
+
"payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
|
|
34815
|
+
};
|
|
34816
|
+
function isToolResult(value) {
|
|
34817
|
+
return Array.isArray(value.content);
|
|
34818
|
+
}
|
|
34819
|
+
function rejection2(data) {
|
|
34820
|
+
return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
|
|
34821
|
+
}
|
|
34822
|
+
async function tokenConfirmation(action, fb) {
|
|
34823
|
+
const subject = await fb.subject();
|
|
34824
|
+
if (isToolResult(subject))
|
|
34825
|
+
return subject;
|
|
34826
|
+
const binding = {
|
|
34827
|
+
tool: fb.tool,
|
|
34828
|
+
...fb.account === void 0 ? {} : { account: fb.account },
|
|
34829
|
+
target: subject.target,
|
|
34830
|
+
...subject.revision === void 0 ? {} : { revision: subject.revision },
|
|
34831
|
+
payloadHash: hashConfirmPayload(subject.payload)
|
|
34832
|
+
};
|
|
34833
|
+
const phaseOne = () => {
|
|
34834
|
+
const { token, expiresAt } = issueConfirmToken(fb.key, binding, { ttlSeconds: fb.ttlSeconds });
|
|
34835
|
+
return {
|
|
34836
|
+
action,
|
|
34837
|
+
preview: subject.preview,
|
|
34838
|
+
confirmToken: token,
|
|
34839
|
+
expiresAt,
|
|
34840
|
+
ttlSeconds: ttlOf(fb.ttlSeconds),
|
|
34841
|
+
instruction: fb.instruction ?? CONFIRM_TOKEN_INSTRUCTION
|
|
34842
|
+
};
|
|
34843
|
+
};
|
|
34844
|
+
if (!fb.confirmToken) {
|
|
34845
|
+
return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
|
|
34846
|
+
}
|
|
34847
|
+
const verdict = verifyConfirmToken(fb.key, fb.confirmToken, binding, { spent: fb.spent });
|
|
34848
|
+
if (verdict.ok)
|
|
34849
|
+
return void 0;
|
|
34850
|
+
if (verdict.error === "DRAFT_CHANGED") {
|
|
34851
|
+
return rejection2({
|
|
34852
|
+
error: "DRAFT_CHANGED",
|
|
34853
|
+
reason: verdict.reason,
|
|
34854
|
+
note: `${CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
|
|
34855
|
+
...phaseOne()
|
|
34856
|
+
});
|
|
34857
|
+
}
|
|
34858
|
+
return rejection2({ error: verdict.error, action, note: TOKEN_ERROR_NOTE[verdict.error] });
|
|
34859
|
+
}
|
|
34860
|
+
async function requireConfirmationWithFallback(ctx, options) {
|
|
34861
|
+
const { tokenFallback, ...confirmation } = options;
|
|
34862
|
+
if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
|
|
34863
|
+
return tokenConfirmation(options.action, tokenFallback);
|
|
34864
|
+
}
|
|
34865
|
+
return requireConfirmation(ctx, confirmation);
|
|
34866
|
+
}
|
|
34867
|
+
|
|
34868
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
|
|
34869
|
+
import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
|
|
34870
|
+
|
|
34871
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
34872
|
+
import { homedir } from "node:os";
|
|
34873
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
34874
|
+
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
34875
|
+
function readEnvVar(key, opts = {}) {
|
|
34876
|
+
const env = opts.env ?? process.env;
|
|
34877
|
+
const raw = env[key];
|
|
34878
|
+
if (typeof raw === "string") {
|
|
34879
|
+
const trimmed = raw.trim();
|
|
34880
|
+
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
34881
|
+
return trimmed;
|
|
34882
|
+
}
|
|
34883
|
+
}
|
|
34884
|
+
return opts.default;
|
|
34885
|
+
}
|
|
34886
|
+
var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
34887
|
+
var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
34888
|
+
function parseBoolEnv(key, opts = {}) {
|
|
34889
|
+
const fallback = opts.default ?? false;
|
|
34890
|
+
const raw = readEnvVar(key, { env: opts.env });
|
|
34891
|
+
if (raw === void 0)
|
|
34892
|
+
return fallback;
|
|
34893
|
+
const token = raw.toLowerCase();
|
|
34894
|
+
if (TRUE_TOKENS.has(token))
|
|
34895
|
+
return true;
|
|
34896
|
+
if (FALSE_TOKENS.has(token))
|
|
34897
|
+
return false;
|
|
34898
|
+
return fallback;
|
|
34899
|
+
}
|
|
34900
|
+
function expandPath(p) {
|
|
34901
|
+
let expanded = p;
|
|
34902
|
+
if (p === "~") {
|
|
34903
|
+
expanded = homedir();
|
|
34904
|
+
} else if (p.startsWith("~/")) {
|
|
34905
|
+
expanded = join(homedir(), p.slice(2));
|
|
34906
|
+
}
|
|
34907
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
34908
|
+
}
|
|
34909
|
+
|
|
34910
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
|
|
34911
|
+
var MODES = /* @__PURE__ */ new Set(["ask-user", "auto", "refuse"]);
|
|
34912
|
+
var DEFAULT_TTL_SECONDS2 = 600;
|
|
34913
|
+
var warned = /* @__PURE__ */ new Set();
|
|
34914
|
+
function readConfirmMode(env = process.env) {
|
|
34915
|
+
const raw = readEnvVar("MCP_CONFIRM_MODE", { env })?.trim().toLowerCase();
|
|
34916
|
+
if (!raw)
|
|
34917
|
+
return "ask-user";
|
|
34918
|
+
if (MODES.has(raw))
|
|
34919
|
+
return raw;
|
|
34920
|
+
if (!warned.has(raw)) {
|
|
34921
|
+
warned.add(raw);
|
|
34922
|
+
process.stderr.write(`MCP_CONFIRM_MODE="${raw}" is not one of ask-user, auto, refuse; treating it as refuse.
|
|
34923
|
+
`);
|
|
34924
|
+
}
|
|
34925
|
+
return "refuse";
|
|
34926
|
+
}
|
|
34927
|
+
function confirmTtlFromEnv(env = process.env) {
|
|
34928
|
+
const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
|
|
34929
|
+
return raw && /^[1-9]\d*$/.test(raw) ? Number(raw) : DEFAULT_TTL_SECONDS2;
|
|
34930
|
+
}
|
|
34931
|
+
var processKey;
|
|
34932
|
+
function confirmKeyFromEnv(env = process.env) {
|
|
34933
|
+
const secret = readEnvVar("MCP_CONFIRM_SECRET", { env });
|
|
34934
|
+
if (secret)
|
|
34935
|
+
return createHash3("sha256").update(secret, "utf8").digest();
|
|
34936
|
+
processKey ??= randomBytes2(32);
|
|
34937
|
+
return processKey;
|
|
34938
|
+
}
|
|
34939
|
+
var CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
|
|
34940
|
+
var REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
|
|
34941
|
+
function confirmationFromEnv(options) {
|
|
34942
|
+
const { tool, account, confirmToken, subject, instruction, spent: spent2, env = process.env, ...confirmation } = options;
|
|
34943
|
+
const mode = readConfirmMode(env);
|
|
34944
|
+
if (mode === "refuse") {
|
|
34945
|
+
return {
|
|
34946
|
+
...confirmation,
|
|
34947
|
+
unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${REFUSE_HINT}` : REFUSE_HINT
|
|
34948
|
+
};
|
|
34949
|
+
}
|
|
34950
|
+
return {
|
|
34951
|
+
...confirmation,
|
|
34952
|
+
tokenFallback: {
|
|
34953
|
+
key: confirmKeyFromEnv(env),
|
|
34954
|
+
tool,
|
|
34955
|
+
...account === void 0 ? {} : { account },
|
|
34956
|
+
...confirmToken === void 0 ? {} : { confirmToken },
|
|
34957
|
+
subject,
|
|
34958
|
+
ttlSeconds: confirmTtlFromEnv(env),
|
|
34959
|
+
instruction: mode === "auto" ? CONFIRM_TOKEN_AUTO_INSTRUCTION : instruction ?? CONFIRM_TOKEN_INSTRUCTION,
|
|
34960
|
+
...spent2 === void 0 ? {} : { spent: spent2 }
|
|
34961
|
+
}
|
|
34962
|
+
};
|
|
34963
|
+
}
|
|
34964
|
+
|
|
34703
34965
|
// ../../node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
34704
34966
|
var SERVER_PROTOCOL_VERSIONS = Object.freeze([
|
|
34705
34967
|
"2026-07-28",
|
|
@@ -34875,45 +35137,6 @@ function runMcp(opts) {
|
|
|
34875
35137
|
return handle;
|
|
34876
35138
|
}
|
|
34877
35139
|
|
|
34878
|
-
// ../../node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
34879
|
-
import { homedir } from "node:os";
|
|
34880
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
34881
|
-
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
34882
|
-
function readEnvVar(key, opts = {}) {
|
|
34883
|
-
const env = opts.env ?? process.env;
|
|
34884
|
-
const raw = env[key];
|
|
34885
|
-
if (typeof raw === "string") {
|
|
34886
|
-
const trimmed = raw.trim();
|
|
34887
|
-
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
34888
|
-
return trimmed;
|
|
34889
|
-
}
|
|
34890
|
-
}
|
|
34891
|
-
return opts.default;
|
|
34892
|
-
}
|
|
34893
|
-
var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
34894
|
-
var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
34895
|
-
function parseBoolEnv(key, opts = {}) {
|
|
34896
|
-
const fallback = opts.default ?? false;
|
|
34897
|
-
const raw = readEnvVar(key, { env: opts.env });
|
|
34898
|
-
if (raw === void 0)
|
|
34899
|
-
return fallback;
|
|
34900
|
-
const token = raw.toLowerCase();
|
|
34901
|
-
if (TRUE_TOKENS.has(token))
|
|
34902
|
-
return true;
|
|
34903
|
-
if (FALSE_TOKENS.has(token))
|
|
34904
|
-
return false;
|
|
34905
|
-
return fallback;
|
|
34906
|
-
}
|
|
34907
|
-
function expandPath(p) {
|
|
34908
|
-
let expanded = p;
|
|
34909
|
-
if (p === "~") {
|
|
34910
|
-
expanded = homedir();
|
|
34911
|
-
} else if (p.startsWith("~/")) {
|
|
34912
|
-
expanded = join(homedir(), p.slice(2));
|
|
34913
|
-
}
|
|
34914
|
-
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
34915
|
-
}
|
|
34916
|
-
|
|
34917
35140
|
// ../../node_modules/@chrischall/mcp-utils/dist/fs/confine.js
|
|
34918
35141
|
import { existsSync, realpathSync } from "node:fs";
|
|
34919
35142
|
import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
|
|
@@ -34995,9 +35218,9 @@ function offsetAt(instant, tz) {
|
|
|
34995
35218
|
const w = wallPartsIn(instant, tz);
|
|
34996
35219
|
const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
|
|
34997
35220
|
const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
|
|
34998
|
-
const
|
|
35221
|
+
const sign2 = minutes < 0 ? "-" : "+";
|
|
34999
35222
|
const abs = Math.abs(minutes);
|
|
35000
|
-
return `${
|
|
35223
|
+
return `${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
35001
35224
|
}
|
|
35002
35225
|
function wallPartsIn(instant, tz) {
|
|
35003
35226
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
@@ -35843,13 +36066,108 @@ function assertNotBoth(inlineParam, fileParam, inlineValue, fileValue) {
|
|
|
35843
36066
|
}
|
|
35844
36067
|
}
|
|
35845
36068
|
|
|
36069
|
+
// src/dispatch-confirmation.ts
|
|
36070
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
36071
|
+
import { readFileSync } from "node:fs";
|
|
36072
|
+
|
|
36073
|
+
// src/send-confirm-token.ts
|
|
36074
|
+
var spent = createSpentTokenStore();
|
|
36075
|
+
function confirmSpentStore() {
|
|
36076
|
+
return spent;
|
|
36077
|
+
}
|
|
36078
|
+
|
|
36079
|
+
// src/dispatch-confirmation.ts
|
|
36080
|
+
var BODY_PREVIEW_MAX = 2048;
|
|
36081
|
+
function bodyPreview(text) {
|
|
36082
|
+
if (!text) return void 0;
|
|
36083
|
+
if (text.length <= BODY_PREVIEW_MAX) return text;
|
|
36084
|
+
return `${text.slice(0, BODY_PREVIEW_MAX)}\u2026 [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
|
|
36085
|
+
}
|
|
36086
|
+
function attachmentNames(paths, inline) {
|
|
36087
|
+
return [...paths ?? [], ...(inline ?? []).map((a) => a.filename)];
|
|
36088
|
+
}
|
|
36089
|
+
function attachmentDetails(paths, inline) {
|
|
36090
|
+
const out = [];
|
|
36091
|
+
for (const path of paths ?? []) {
|
|
36092
|
+
let bytes;
|
|
36093
|
+
try {
|
|
36094
|
+
bytes = readFileSync(path);
|
|
36095
|
+
} catch {
|
|
36096
|
+
bytes = void 0;
|
|
36097
|
+
}
|
|
36098
|
+
out.push(bytes ? { name: path, size: bytes.length, sha256: createHash4("sha256").update(bytes).digest("hex") } : { name: path, size: null });
|
|
36099
|
+
}
|
|
36100
|
+
for (const a of inline ?? []) {
|
|
36101
|
+
out.push({
|
|
36102
|
+
name: a.filename,
|
|
36103
|
+
size: Buffer.from(a.contentBase64, "base64").length,
|
|
36104
|
+
sha256: createHash4("sha256").update(a.contentBase64).digest("hex")
|
|
36105
|
+
});
|
|
36106
|
+
}
|
|
36107
|
+
return out;
|
|
36108
|
+
}
|
|
36109
|
+
function senderPreview(account, from) {
|
|
36110
|
+
return from ?? account ?? readEnvVar("GOG_ACCOUNT") ?? "the gog account's default address";
|
|
36111
|
+
}
|
|
36112
|
+
function attachmentPreview(details) {
|
|
36113
|
+
return details.map(({ name, size }) => ({ name, size }));
|
|
36114
|
+
}
|
|
36115
|
+
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.";
|
|
36116
|
+
var CONFIRM_ACTION_INSTRUCTION = CONFIRM_TOKEN_INSTRUCTION;
|
|
36117
|
+
var CONFIRM_FALLBACK_DESCRIPTION = ' If the client cannot show that prompt (no MCP elicitation, e.g. claude.ai), a two-step flow applies instead: call WITHOUT confirmToken and nothing is sent or changed \u2014 the result has status "confirmation-required", the full preview and a confirmToken. Follow its instruction (by default: show the preview to the user verbatim and only after they explicitly approve it in chat, call again with the SAME arguments plus confirmToken). The tool re-reads what it would act on and refuses (DRAFT_CHANGED, with a fresh preview and token) if it changed; TOKEN_EXPIRED / TOKEN_REUSED / TOKEN_INVALID also do nothing. A server set to MCP_CONFIRM_MODE=refuse refuses instead.';
|
|
36118
|
+
function gatedElsewhere(what, via, does, tool) {
|
|
36119
|
+
return `${what} ${does} and is not available through ${via}. Use ${tool}, which asks the user to confirm.`;
|
|
36120
|
+
}
|
|
36121
|
+
function hasCommandWord(args, words) {
|
|
36122
|
+
return args.find((a) => words.has(a.toLowerCase()));
|
|
36123
|
+
}
|
|
36124
|
+
async function requireDispatchConfirmation(ctx, options) {
|
|
36125
|
+
const { action, fallback } = options;
|
|
36126
|
+
const confirmation = {
|
|
36127
|
+
action,
|
|
36128
|
+
message: options.message,
|
|
36129
|
+
details: options.details,
|
|
36130
|
+
confirmationLabel: options.confirmationLabel,
|
|
36131
|
+
...options.unsupportedNote ? { unsupportedNote: options.unsupportedNote } : {}
|
|
36132
|
+
};
|
|
36133
|
+
if (!fallback) return requireConfirmationWithFallback(ctx, confirmation);
|
|
36134
|
+
return requireConfirmationWithFallback(ctx, confirmationFromEnv({
|
|
36135
|
+
...confirmation,
|
|
36136
|
+
tool: fallback.tool,
|
|
36137
|
+
account: fallback.account ?? readEnvVar("GOG_ACCOUNT") ?? "",
|
|
36138
|
+
confirmToken: fallback.confirmToken,
|
|
36139
|
+
subject: fallback.subject,
|
|
36140
|
+
instruction: fallback.instruction ?? CONFIRM_ACTION_INSTRUCTION,
|
|
36141
|
+
spent: confirmSpentStore()
|
|
36142
|
+
}));
|
|
36143
|
+
}
|
|
36144
|
+
function resultText(result) {
|
|
36145
|
+
const first = result.content[0];
|
|
36146
|
+
return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
|
|
36147
|
+
}
|
|
36148
|
+
|
|
35846
36149
|
// src/tools/api.ts
|
|
35847
36150
|
var GMAIL_API_BLOCKED = /(?:\.send$|forwarding|filters\.create|filters\.update|delegates\.create)/i;
|
|
36151
|
+
var DISPATCH_API_BLOCKED = {
|
|
36152
|
+
chat: [{ method: /(?:^|\.)spaces\.messages\.create$/i, does: "posts a Chat message", tool: "gog_chat_messages_send / gog_chat_dm_send" }],
|
|
36153
|
+
drive: [{ method: /(?:^|\.)permissions\.(?:create|update)$/i, does: "grants access to a file", tool: "gog_drive_share" }],
|
|
36154
|
+
classroom: [
|
|
36155
|
+
{ method: /(?:^|\.)courses\.announcements\.create$/i, does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
36156
|
+
{ method: /(?:^|\.)invitations\.create$/i, does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
|
|
36157
|
+
],
|
|
36158
|
+
calendar: [
|
|
36159
|
+
{ method: /(?:^|\.)events\.(?:insert|import|quickadd)$/i, does: "can put an event on guests' calendars", tool: "gog_calendar_create" },
|
|
36160
|
+
{ method: /(?:^|\.)events\.(?:update|patch)$/i, does: "can change what guests see", tool: "gog_calendar_update / gog_calendar_respond" }
|
|
36161
|
+
]
|
|
36162
|
+
};
|
|
35848
36163
|
function refusedApiCall(api, method) {
|
|
35849
|
-
|
|
36164
|
+
const name = api.trim().toLowerCase();
|
|
36165
|
+
const m = method.trim();
|
|
36166
|
+
if (name === "gmail" && GMAIL_API_BLOCKED.test(m)) {
|
|
35850
36167
|
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
36168
|
}
|
|
35852
|
-
|
|
36169
|
+
const hit = (Object.hasOwn(DISPATCH_API_BLOCKED, name) ? DISPATCH_API_BLOCKED[name] : []).find((b) => b.method.test(m));
|
|
36170
|
+
return hit ? gatedElsewhere(`${name} ${m}`, "gog_api_call", hit.does, hit.tool) : void 0;
|
|
35853
36171
|
}
|
|
35854
36172
|
function registerApiTools(server) {
|
|
35855
36173
|
server.registerTool("gog_api_list", {
|
|
@@ -35879,7 +36197,7 @@ function registerApiTools(server) {
|
|
|
35879
36197
|
return runOrDiagnose(args, { account });
|
|
35880
36198
|
});
|
|
35881
36199
|
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.",
|
|
36200
|
+
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
36201
|
annotations: { destructiveHint: true },
|
|
35884
36202
|
inputSchema: external_exports.object({
|
|
35885
36203
|
api: external_exports.string().describe("Discovery API name (e.g. drive, gmail, calendar)"),
|
|
@@ -36162,6 +36480,54 @@ function registerAuthTools(server) {
|
|
|
36162
36480
|
}
|
|
36163
36481
|
|
|
36164
36482
|
// src/tools/calendar.ts
|
|
36483
|
+
var CALENDAR_GATED = {
|
|
36484
|
+
create: "gog_calendar_create",
|
|
36485
|
+
add: "gog_calendar_create",
|
|
36486
|
+
new: "gog_calendar_create",
|
|
36487
|
+
update: "gog_calendar_update",
|
|
36488
|
+
edit: "gog_calendar_update",
|
|
36489
|
+
set: "gog_calendar_update",
|
|
36490
|
+
respond: "gog_calendar_respond",
|
|
36491
|
+
rsvp: "gog_calendar_respond",
|
|
36492
|
+
reply: "gog_calendar_respond"
|
|
36493
|
+
};
|
|
36494
|
+
function vetCalendarRun(subcommand, _args) {
|
|
36495
|
+
const sub = subcommand.toLowerCase();
|
|
36496
|
+
const tool = Object.hasOwn(CALENDAR_GATED, sub) ? CALENDAR_GATED[sub] : void 0;
|
|
36497
|
+
return tool ? gatedElsewhere(`gog calendar ${sub}`, "gog_calendar_run", "can change what guests see", tool) : void 0;
|
|
36498
|
+
}
|
|
36499
|
+
function parseAttendees(value) {
|
|
36500
|
+
if (!value) return [];
|
|
36501
|
+
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);
|
|
36502
|
+
}
|
|
36503
|
+
function timeOf(t) {
|
|
36504
|
+
const v = t?.dateTime ?? t?.date;
|
|
36505
|
+
return typeof v === "string" ? v : void 0;
|
|
36506
|
+
}
|
|
36507
|
+
function eventSnapshot(raw) {
|
|
36508
|
+
let event;
|
|
36509
|
+
try {
|
|
36510
|
+
event = JSON.parse(raw)?.event;
|
|
36511
|
+
} catch {
|
|
36512
|
+
event = void 0;
|
|
36513
|
+
}
|
|
36514
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
36515
|
+
return {
|
|
36516
|
+
summary: str(event?.summary),
|
|
36517
|
+
start: timeOf(event?.start),
|
|
36518
|
+
end: timeOf(event?.end),
|
|
36519
|
+
organizer: str(event?.organizer?.email),
|
|
36520
|
+
// Guests are the OTHER people: not this account's own attendee row, not rooms.
|
|
36521
|
+
guests: (Array.isArray(event?.attendees) ? event.attendees : []).filter((a) => a.self !== true && a.resource !== true && typeof a.email === "string").map((a) => a.email),
|
|
36522
|
+
etag: str(event?.etag)
|
|
36523
|
+
};
|
|
36524
|
+
}
|
|
36525
|
+
async function readEvent(calendarId, eventId, account) {
|
|
36526
|
+
const got = await runOrDiagnose(["calendar", "event", pos(calendarId), pos(eventId)], { account });
|
|
36527
|
+
if (got.isError) return { error: got };
|
|
36528
|
+
return { event: eventSnapshot(resultText(got)) };
|
|
36529
|
+
}
|
|
36530
|
+
var CALENDAR_CONFIRM_LABEL = "Confirm that this change, which other people will see, should be made now.";
|
|
36165
36531
|
var reminderParams = {
|
|
36166
36532
|
reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
|
|
36167
36533
|
`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 +36615,7 @@ function registerCalendarTools(server) {
|
|
|
36249
36615
|
return runOrDiagnose(args, { account });
|
|
36250
36616
|
});
|
|
36251
36617
|
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).",
|
|
36618
|
+
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
36619
|
annotations: { destructiveHint: false },
|
|
36254
36620
|
inputSchema: external_exports.object({
|
|
36255
36621
|
calendarId: external_exports.string().describe('Calendar ID (use "primary" for the default calendar)'),
|
|
@@ -36263,9 +36629,10 @@ function registerCalendarTools(server) {
|
|
|
36263
36629
|
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
36630
|
withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)"),
|
|
36265
36631
|
...reminderParams,
|
|
36266
|
-
account: accountParam
|
|
36632
|
+
account: accountParam,
|
|
36633
|
+
confirmToken: confirmTokenParam
|
|
36267
36634
|
})
|
|
36268
|
-
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
|
|
36635
|
+
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account, confirmToken }, ctx) => {
|
|
36269
36636
|
const args = ["calendar", "create", pos(calendarId), `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
|
|
36270
36637
|
if (description) args.push(`--description=${description}`);
|
|
36271
36638
|
if (location) args.push(`--location=${location}`);
|
|
@@ -36274,10 +36641,28 @@ function registerCalendarTools(server) {
|
|
|
36274
36641
|
if (timezone) args.push(`--timezone=${timezone}`);
|
|
36275
36642
|
if (withZoom) args.push("--with-zoom");
|
|
36276
36643
|
pushReminderFlags(args, { reminders, noReminders });
|
|
36644
|
+
const guests = parseAttendees(attendees);
|
|
36645
|
+
if (guests.length > 0) {
|
|
36646
|
+
const event = { calendarId, summary, from, to, allDay: Boolean(allDay), timezone, location, description, guests, attendees, withZoom: Boolean(withZoom) };
|
|
36647
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
36648
|
+
action: "calendar.create",
|
|
36649
|
+
message: "Review and confirm this event, which will appear on the guests' calendars:",
|
|
36650
|
+
confirmationLabel: CALENDAR_CONFIRM_LABEL,
|
|
36651
|
+
details: event,
|
|
36652
|
+
unsupportedNote: "Create it without attendees instead; the user can add the guests in Google Calendar.",
|
|
36653
|
+
fallback: {
|
|
36654
|
+
tool: "gog_calendar_create",
|
|
36655
|
+
account,
|
|
36656
|
+
confirmToken,
|
|
36657
|
+
subject: () => ({ target: calendarId, payload: event, preview: event })
|
|
36658
|
+
}
|
|
36659
|
+
});
|
|
36660
|
+
if (confirmation) return confirmation;
|
|
36661
|
+
}
|
|
36277
36662
|
return runOrDiagnose(args, { account });
|
|
36278
36663
|
});
|
|
36279
36664
|
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.",
|
|
36665
|
+
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
36666
|
annotations: { destructiveHint: false },
|
|
36282
36667
|
inputSchema: external_exports.object({
|
|
36283
36668
|
calendarId: external_exports.string().describe("Calendar ID"),
|
|
@@ -36295,9 +36680,10 @@ function registerCalendarTools(server) {
|
|
|
36295
36680
|
removeZoom: external_exports.boolean().optional().describe("Remove the event's Zoom video conference"),
|
|
36296
36681
|
removeMeet: external_exports.boolean().optional().describe("Remove the event's Google Meet video conference (clears conference data only)"),
|
|
36297
36682
|
...reminderParams,
|
|
36298
|
-
account: accountParam
|
|
36683
|
+
account: accountParam,
|
|
36684
|
+
confirmToken: confirmTokenParam
|
|
36299
36685
|
})
|
|
36300
|
-
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
|
|
36686
|
+
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account, confirmToken }, ctx) => {
|
|
36301
36687
|
const args = ["calendar", "update", pos(calendarId), pos(eventId)];
|
|
36302
36688
|
if (summary !== void 0) args.push(`--summary=${summary}`);
|
|
36303
36689
|
if (from !== void 0) args.push(`--from=${from}`);
|
|
@@ -36312,6 +36698,45 @@ function registerCalendarTools(server) {
|
|
|
36312
36698
|
if (removeZoom) args.push("--remove-zoom");
|
|
36313
36699
|
if (removeMeet) args.push("--remove-meet");
|
|
36314
36700
|
pushReminderFlags(args, { reminders, noReminders });
|
|
36701
|
+
const changes = Object.fromEntries(Object.entries({
|
|
36702
|
+
summary,
|
|
36703
|
+
from,
|
|
36704
|
+
to,
|
|
36705
|
+
description,
|
|
36706
|
+
location,
|
|
36707
|
+
attendees,
|
|
36708
|
+
addAttendees,
|
|
36709
|
+
attachments,
|
|
36710
|
+
withZoom,
|
|
36711
|
+
regenerateZoom,
|
|
36712
|
+
removeZoom,
|
|
36713
|
+
removeMeet
|
|
36714
|
+
}).filter(([, v]) => v !== void 0 && v !== false));
|
|
36715
|
+
if (Object.keys(changes).length > 0) {
|
|
36716
|
+
const read = await readEvent(calendarId, eventId, account);
|
|
36717
|
+
if (read.error) return read.error;
|
|
36718
|
+
const { etag, ...current } = read.event;
|
|
36719
|
+
const added = [...parseAttendees(attendees), ...parseAttendees(addAttendees)];
|
|
36720
|
+
if (current.guests.length > 0 || added.length > 0) {
|
|
36721
|
+
const view = { calendarId, eventId, current, changes };
|
|
36722
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
36723
|
+
action: "calendar.update",
|
|
36724
|
+
message: "Review and confirm this change to an event other people are on:",
|
|
36725
|
+
confirmationLabel: CALENDAR_CONFIRM_LABEL,
|
|
36726
|
+
details: view,
|
|
36727
|
+
unsupportedNote: "Ask the user to make this change in Google Calendar.",
|
|
36728
|
+
fallback: {
|
|
36729
|
+
tool: "gog_calendar_update",
|
|
36730
|
+
account,
|
|
36731
|
+
confirmToken,
|
|
36732
|
+
// The etag rotates on any edit, so an event changed elsewhere between
|
|
36733
|
+
// the two phases is DRAFT_CHANGED rather than edited over.
|
|
36734
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
|
|
36735
|
+
}
|
|
36736
|
+
});
|
|
36737
|
+
if (confirmation) return confirmation;
|
|
36738
|
+
}
|
|
36739
|
+
}
|
|
36315
36740
|
return runOrDiagnose(args, { account });
|
|
36316
36741
|
});
|
|
36317
36742
|
server.registerTool("gog_calendar_delete", {
|
|
@@ -36326,21 +36751,40 @@ function registerCalendarTools(server) {
|
|
|
36326
36751
|
return runOrDiagnose(["calendar", "delete", pos(calendarId), pos(eventId), "--force"], { account });
|
|
36327
36752
|
});
|
|
36328
36753
|
server.registerTool("gog_calendar_respond", {
|
|
36329
|
-
description: "Respond to a calendar event invitation.",
|
|
36754
|
+
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
36755
|
annotations: { destructiveHint: true },
|
|
36331
36756
|
inputSchema: external_exports.object({
|
|
36332
36757
|
calendarId: external_exports.string().describe("Calendar ID"),
|
|
36333
36758
|
eventId: external_exports.string().describe("Event ID"),
|
|
36334
36759
|
status: external_exports.enum(["accepted", "declined", "tentative"]).describe("Response status"),
|
|
36335
36760
|
comment: external_exports.string().optional().describe("Optional comment to include with response"),
|
|
36336
|
-
account: accountParam
|
|
36761
|
+
account: accountParam,
|
|
36762
|
+
confirmToken: confirmTokenParam
|
|
36337
36763
|
})
|
|
36338
|
-
}, async ({ calendarId, eventId, status, comment, account }) => {
|
|
36764
|
+
}, async ({ calendarId, eventId, status, comment, account, confirmToken }, ctx) => {
|
|
36339
36765
|
const args = ["calendar", "respond", pos(calendarId), pos(eventId), `--status=${status}`];
|
|
36340
36766
|
if (comment) args.push(`--comment=${comment}`);
|
|
36767
|
+
const read = await readEvent(calendarId, eventId, account);
|
|
36768
|
+
if (read.error) return read.error;
|
|
36769
|
+
const { etag, ...event } = read.event;
|
|
36770
|
+
const view = { calendarId, eventId, event, response: status, ...comment ? { comment } : {} };
|
|
36771
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
36772
|
+
action: "calendar.respond",
|
|
36773
|
+
message: "Review and confirm this response, which the organizer will see:",
|
|
36774
|
+
confirmationLabel: "Confirm that this response should be recorded now.",
|
|
36775
|
+
details: view,
|
|
36776
|
+
unsupportedNote: "Ask the user to respond from Google Calendar.",
|
|
36777
|
+
fallback: {
|
|
36778
|
+
tool: "gog_calendar_respond",
|
|
36779
|
+
account,
|
|
36780
|
+
confirmToken,
|
|
36781
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
|
|
36782
|
+
}
|
|
36783
|
+
});
|
|
36784
|
+
if (confirmation) return confirmation;
|
|
36341
36785
|
return runOrDiagnose(args, { account });
|
|
36342
36786
|
});
|
|
36343
|
-
registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"' });
|
|
36787
|
+
registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"', vet: vetCalendarRun });
|
|
36344
36788
|
}
|
|
36345
36789
|
|
|
36346
36790
|
// src/attachments.ts
|
|
@@ -36424,6 +36868,23 @@ function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
|
|
|
36424
36868
|
}
|
|
36425
36869
|
|
|
36426
36870
|
// src/tools/chat.ts
|
|
36871
|
+
var CHAT_SEND_WORDS = /* @__PURE__ */ new Set(["send", "create", "post"]);
|
|
36872
|
+
function vetChatRun(subcommand, args) {
|
|
36873
|
+
const sub = subcommand.toLowerCase();
|
|
36874
|
+
if (sub !== "messages" && sub !== "dm") return void 0;
|
|
36875
|
+
const word = hasCommandWord(args, CHAT_SEND_WORDS);
|
|
36876
|
+
if (!word) return void 0;
|
|
36877
|
+
return gatedElsewhere(
|
|
36878
|
+
`gog chat ${sub} ${word.toLowerCase()}`,
|
|
36879
|
+
"gog_chat_run",
|
|
36880
|
+
"posts a message",
|
|
36881
|
+
sub === "dm" ? "gog_chat_dm_send" : "gog_chat_messages_send"
|
|
36882
|
+
);
|
|
36883
|
+
}
|
|
36884
|
+
var CHAT_CONFIRM = {
|
|
36885
|
+
message: "Review and confirm this Google Chat message:",
|
|
36886
|
+
confirmationLabel: "Confirm that this Chat message should be posted now."
|
|
36887
|
+
};
|
|
36427
36888
|
function registerChatTools(server) {
|
|
36428
36889
|
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
36890
|
const spaceParam = external_exports.string().describe(
|
|
@@ -36529,7 +36990,7 @@ function registerChatTools(server) {
|
|
|
36529
36990
|
return runOrDiagnose(args, { account });
|
|
36530
36991
|
});
|
|
36531
36992
|
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,
|
|
36993
|
+
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
36994
|
annotations: { destructiveHint: true },
|
|
36534
36995
|
inputSchema: external_exports.object({
|
|
36535
36996
|
space: spaceParam,
|
|
@@ -36539,9 +37000,10 @@ function registerChatTools(server) {
|
|
|
36539
37000
|
"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
37001
|
),
|
|
36541
37002
|
attachInline: attachInlineParam,
|
|
36542
|
-
account: accountParam
|
|
37003
|
+
account: accountParam,
|
|
37004
|
+
confirmToken: confirmTokenParam
|
|
36543
37005
|
})
|
|
36544
|
-
}, async ({ space, text, thread, attach, attachInline, account }) => {
|
|
37006
|
+
}, async ({ space, text, thread, attach, attachInline, account, confirmToken }, ctx) => {
|
|
36545
37007
|
confinePaths(attach, "attach");
|
|
36546
37008
|
if (text === void 0 && !attach?.length && !attachInline?.length) {
|
|
36547
37009
|
throw new Error("A Chat message needs text, an attachment, or both.");
|
|
@@ -36551,20 +37013,56 @@ function registerChatTools(server) {
|
|
|
36551
37013
|
if (thread) args.push(`--thread=${thread}`);
|
|
36552
37014
|
if (attach) for (const path of attach) args.push(`--attach=${path}`);
|
|
36553
37015
|
args.push(...inlineAttachmentArgs("attach", attachInline, args));
|
|
37016
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
37017
|
+
...CHAT_CONFIRM,
|
|
37018
|
+
action: "chat.message-send",
|
|
37019
|
+
details: { space, thread, textPreview: bodyPreview(text), attachments: attachmentNames(attach, attachInline) },
|
|
37020
|
+
fallback: {
|
|
37021
|
+
tool: "gog_chat_messages_send",
|
|
37022
|
+
account,
|
|
37023
|
+
confirmToken,
|
|
37024
|
+
subject: () => {
|
|
37025
|
+
const attachments = attachmentDetails(attach, attachInline);
|
|
37026
|
+
const from = senderPreview(account);
|
|
37027
|
+
return {
|
|
37028
|
+
target: space,
|
|
37029
|
+
payload: { from, space, thread, text, attachments },
|
|
37030
|
+
preview: { from, space, thread, text, attachments: attachmentPreview(attachments) }
|
|
37031
|
+
};
|
|
37032
|
+
}
|
|
37033
|
+
}
|
|
37034
|
+
});
|
|
37035
|
+
if (confirmation) return confirmation;
|
|
36554
37036
|
return runOrDiagnose(args, { account });
|
|
36555
37037
|
});
|
|
36556
37038
|
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,
|
|
37039
|
+
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
37040
|
annotations: { destructiveHint: true },
|
|
36559
37041
|
inputSchema: external_exports.object({
|
|
36560
37042
|
email: external_exports.string().describe("Recipient email address"),
|
|
36561
37043
|
text: external_exports.string().describe("Message text"),
|
|
36562
37044
|
thread: threadParam,
|
|
36563
|
-
account: accountParam
|
|
37045
|
+
account: accountParam,
|
|
37046
|
+
confirmToken: confirmTokenParam
|
|
36564
37047
|
})
|
|
36565
|
-
}, async ({ email: email3, text, thread, account }) => {
|
|
37048
|
+
}, async ({ email: email3, text, thread, account, confirmToken }, ctx) => {
|
|
36566
37049
|
const args = ["chat", "dm", "send", pos(email3), `--text=${text}`];
|
|
36567
37050
|
if (thread) args.push(`--thread=${thread}`);
|
|
37051
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
37052
|
+
...CHAT_CONFIRM,
|
|
37053
|
+
action: "chat.dm-send",
|
|
37054
|
+
details: { to: email3, thread, textPreview: bodyPreview(text) },
|
|
37055
|
+
fallback: {
|
|
37056
|
+
tool: "gog_chat_dm_send",
|
|
37057
|
+
account,
|
|
37058
|
+
confirmToken,
|
|
37059
|
+
subject: () => {
|
|
37060
|
+
const from = senderPreview(account);
|
|
37061
|
+
return { target: email3, payload: { from, to: email3, thread, text }, preview: { from, to: email3, thread, text } };
|
|
37062
|
+
}
|
|
37063
|
+
}
|
|
37064
|
+
});
|
|
37065
|
+
if (confirmation) return confirmation;
|
|
36568
37066
|
return runOrDiagnose(args, { account });
|
|
36569
37067
|
});
|
|
36570
37068
|
server.registerTool("gog_chat_dm_space", {
|
|
@@ -36619,11 +37117,45 @@ function registerChatTools(server) {
|
|
|
36619
37117
|
registerRunTool(server, {
|
|
36620
37118
|
service: "chat",
|
|
36621
37119
|
examples: '"spaces", "messages", "dm"',
|
|
37120
|
+
vet: vetChatRun,
|
|
36622
37121
|
note: "Google Chat has no API for consumer accounts: every chat subcommand fails on an @gmail.com account regardless of scopes."
|
|
36623
37122
|
});
|
|
36624
37123
|
}
|
|
36625
37124
|
|
|
36626
37125
|
// src/tools/classroom.ts
|
|
37126
|
+
var CLASSROOM_CREATE_WORDS = /* @__PURE__ */ new Set(["create", "add", "new"]);
|
|
37127
|
+
var CLASSROOM_GATED = {
|
|
37128
|
+
announcements: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
37129
|
+
announcement: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
37130
|
+
ann: { does: "posts to a class", tool: "gog_classroom_announcements_create" },
|
|
37131
|
+
invitations: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
|
|
37132
|
+
invitation: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" },
|
|
37133
|
+
invites: { does: "invites someone to a class", tool: "gog_classroom_invitations_create" }
|
|
37134
|
+
};
|
|
37135
|
+
function vetClassroomRun(subcommand, args) {
|
|
37136
|
+
const sub = subcommand.toLowerCase();
|
|
37137
|
+
const gated = Object.hasOwn(CLASSROOM_GATED, sub) ? CLASSROOM_GATED[sub] : void 0;
|
|
37138
|
+
if (!gated) return void 0;
|
|
37139
|
+
const word = hasCommandWord(args, CLASSROOM_CREATE_WORDS);
|
|
37140
|
+
return word ? gatedElsewhere(`gog classroom ${sub} ${word.toLowerCase()}`, "gog_classroom_run", gated.does, gated.tool) : void 0;
|
|
37141
|
+
}
|
|
37142
|
+
async function readCourse(courseId, account, runner = runOrDiagnose) {
|
|
37143
|
+
const got = await runner(["classroom", "courses", "get", pos(courseId)], { account });
|
|
37144
|
+
if (got.isError) return { error: got };
|
|
37145
|
+
let course;
|
|
37146
|
+
try {
|
|
37147
|
+
course = JSON.parse(resultText(got))?.course;
|
|
37148
|
+
} catch {
|
|
37149
|
+
course = void 0;
|
|
37150
|
+
}
|
|
37151
|
+
return {
|
|
37152
|
+
course: {
|
|
37153
|
+
id: courseId,
|
|
37154
|
+
...typeof course?.name === "string" ? { name: course.name } : {},
|
|
37155
|
+
...typeof course?.section === "string" ? { section: course.section } : {}
|
|
37156
|
+
}
|
|
37157
|
+
};
|
|
37158
|
+
}
|
|
36627
37159
|
function registerClassroomTools(server) {
|
|
36628
37160
|
server.registerTool("gog_classroom_courses_list", {
|
|
36629
37161
|
description: "List Google Classroom courses.",
|
|
@@ -36906,19 +37438,42 @@ function registerClassroomTools(server) {
|
|
|
36906
37438
|
return runOrDiagnose(["classroom", "announcements", "get", pos(courseId), pos(announcementId)], { account });
|
|
36907
37439
|
});
|
|
36908
37440
|
server.registerTool("gog_classroom_announcements_create", {
|
|
36909
|
-
description: "Create an announcement in a Google Classroom course.",
|
|
37441
|
+
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
37442
|
annotations: { destructiveHint: true },
|
|
36911
37443
|
inputSchema: external_exports.object({
|
|
36912
37444
|
courseId: external_exports.string().describe("Course ID"),
|
|
36913
37445
|
text: external_exports.string().describe("Announcement text"),
|
|
36914
|
-
state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State"),
|
|
37446
|
+
state: external_exports.enum(["PUBLISHED", "DRAFT"]).optional().describe("State (DRAFT is visible only to teachers and needs no confirmation)"),
|
|
36915
37447
|
scheduled: external_exports.string().optional().describe("Scheduled publish time"),
|
|
36916
|
-
account: accountParam
|
|
37448
|
+
account: accountParam,
|
|
37449
|
+
confirmToken: confirmTokenParam
|
|
36917
37450
|
})
|
|
36918
|
-
}, async ({ courseId, text, state, scheduled, account }) => {
|
|
37451
|
+
}, async ({ courseId, text, state, scheduled, account, confirmToken }, ctx) => {
|
|
36919
37452
|
const args = ["classroom", "announcements", "create", pos(courseId), `--text=${text}`];
|
|
36920
37453
|
if (state) args.push(`--state=${state}`);
|
|
36921
37454
|
if (scheduled) args.push(`--scheduled=${scheduled}`);
|
|
37455
|
+
if (state !== "DRAFT") {
|
|
37456
|
+
const read = await readCourse(courseId, account);
|
|
37457
|
+
if (read.error) return read.error;
|
|
37458
|
+
const publishes = scheduled ? `at ${scheduled}` : "immediately";
|
|
37459
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
37460
|
+
action: "classroom.announcement-create",
|
|
37461
|
+
message: "Review and confirm this Classroom announcement:",
|
|
37462
|
+
confirmationLabel: "Confirm that this announcement should be posted to the class.",
|
|
37463
|
+
details: { course: read.course, publishes, textPreview: bodyPreview(text) },
|
|
37464
|
+
unsupportedNote: "Create it with state DRAFT instead; the user can review and post it from Classroom.",
|
|
37465
|
+
fallback: {
|
|
37466
|
+
tool: "gog_classroom_announcements_create",
|
|
37467
|
+
account,
|
|
37468
|
+
confirmToken,
|
|
37469
|
+
subject: () => {
|
|
37470
|
+
const view = { course: read.course, publishes, text };
|
|
37471
|
+
return { target: courseId, payload: view, preview: view };
|
|
37472
|
+
}
|
|
37473
|
+
}
|
|
37474
|
+
});
|
|
37475
|
+
if (confirmation) return confirmation;
|
|
37476
|
+
}
|
|
36922
37477
|
return runOrDiagnose(args, { account });
|
|
36923
37478
|
});
|
|
36924
37479
|
server.registerTool("gog_classroom_topics_list", {
|
|
@@ -37008,6 +37563,7 @@ function registerClassroomTools(server) {
|
|
|
37008
37563
|
registerRunTool(server, {
|
|
37009
37564
|
service: "classroom",
|
|
37010
37565
|
examples: '"guardians", "materials", "guardian-invitations"',
|
|
37566
|
+
vet: vetClassroomRun,
|
|
37011
37567
|
note: "Covers anything not wrapped by the dedicated tools (guardians, guardian-invitations, materials, coursework assignees, announcement assignees, etc.)."
|
|
37012
37568
|
});
|
|
37013
37569
|
}
|
|
@@ -37168,7 +37724,18 @@ function registerDocsTools(server) {
|
|
|
37168
37724
|
}
|
|
37169
37725
|
|
|
37170
37726
|
// src/tools/drive.ts
|
|
37727
|
+
function vetDriveRun(subcommand, _args) {
|
|
37728
|
+
return subcommand.toLowerCase() === "share" ? gatedElsewhere("gog drive share", "gog_drive_run", "grants access to a file", "gog_drive_share") : void 0;
|
|
37729
|
+
}
|
|
37171
37730
|
var GOOGLE_DOC_MIME = "application/vnd.google-apps.document";
|
|
37731
|
+
function shareTargetMeta(raw) {
|
|
37732
|
+
try {
|
|
37733
|
+
const { name, mimeType } = fileMeta(raw);
|
|
37734
|
+
return { name, mimeType };
|
|
37735
|
+
} catch {
|
|
37736
|
+
return {};
|
|
37737
|
+
}
|
|
37738
|
+
}
|
|
37172
37739
|
function fileMeta(raw) {
|
|
37173
37740
|
const parsed = JSON.parse(raw);
|
|
37174
37741
|
const f = parsed.file ?? parsed;
|
|
@@ -37291,7 +37858,7 @@ function registerDriveTools(server) {
|
|
|
37291
37858
|
return runOrDiagnose(args, { account });
|
|
37292
37859
|
});
|
|
37293
37860
|
server.registerTool("gog_drive_share", {
|
|
37294
|
-
description: "Share a Google Drive file or folder.",
|
|
37861
|
+
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
37862
|
annotations: { destructiveHint: true },
|
|
37296
37863
|
inputSchema: external_exports.object({
|
|
37297
37864
|
fileId: external_exports.string().describe("File or folder ID"),
|
|
@@ -37299,9 +37866,35 @@ function registerDriveTools(server) {
|
|
|
37299
37866
|
email: external_exports.string().optional().describe("User email (required when to=user)"),
|
|
37300
37867
|
domain: external_exports.string().optional().describe("Domain (required when to=domain)"),
|
|
37301
37868
|
role: external_exports.enum(["reader", "writer"]).optional().describe("Permission role (default: reader)"),
|
|
37302
|
-
account: accountParam
|
|
37869
|
+
account: accountParam,
|
|
37870
|
+
confirmToken: confirmTokenParam
|
|
37303
37871
|
})
|
|
37304
|
-
}, async ({ fileId, to, email: email3, domain: domain2, role, account }) => {
|
|
37872
|
+
}, async ({ fileId, to, email: email3, domain: domain2, role, account, confirmToken }, ctx) => {
|
|
37873
|
+
const got = await runOrDiagnose(["drive", "get", pos(fileId)], { account });
|
|
37874
|
+
if (got.isError) return got;
|
|
37875
|
+
const file2 = { id: fileId, ...shareTargetMeta(resultText(got)) };
|
|
37876
|
+
const grant = {
|
|
37877
|
+
file: file2,
|
|
37878
|
+
to,
|
|
37879
|
+
...email3 ? { email: email3 } : {},
|
|
37880
|
+
...domain2 ? { domain: domain2 } : {},
|
|
37881
|
+
role: role ?? "reader",
|
|
37882
|
+
publicLink: to === "anyone"
|
|
37883
|
+
};
|
|
37884
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
37885
|
+
action: "drive.share",
|
|
37886
|
+
message: to === "anyone" ? "Review and confirm making this file available to ANYONE with the link:" : "Review and confirm this Drive share:",
|
|
37887
|
+
confirmationLabel: "Confirm that this access should be granted now.",
|
|
37888
|
+
details: grant,
|
|
37889
|
+
unsupportedNote: "Ask the user to share it themselves from Google Drive.",
|
|
37890
|
+
fallback: {
|
|
37891
|
+
tool: "gog_drive_share",
|
|
37892
|
+
account,
|
|
37893
|
+
confirmToken,
|
|
37894
|
+
subject: () => ({ target: fileId, payload: grant, preview: grant })
|
|
37895
|
+
}
|
|
37896
|
+
});
|
|
37897
|
+
if (confirmation) return confirmation;
|
|
37305
37898
|
const args = ["drive", "share", pos(fileId), `--to=${to}`];
|
|
37306
37899
|
if (email3) args.push(`--email=${email3}`);
|
|
37307
37900
|
if (domain2) args.push(`--domain=${domain2}`);
|
|
@@ -37414,7 +38007,7 @@ function registerDriveTools(server) {
|
|
|
37414
38007
|
return diagnose(err);
|
|
37415
38008
|
}
|
|
37416
38009
|
});
|
|
37417
|
-
registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"' });
|
|
38010
|
+
registerRunTool(server, { service: "drive", examples: '"copy", "download", "permissions"', vet: vetDriveRun });
|
|
37418
38011
|
}
|
|
37419
38012
|
|
|
37420
38013
|
// src/gmail-results.ts
|
|
@@ -37540,24 +38133,16 @@ var STAGING_TWIN = {
|
|
|
37540
38133
|
var UNSUPPORTED_NOTE2 = {
|
|
37541
38134
|
"gmail.drafts-send": "The draft is still saved: ask the user to review it and send it from Gmail."
|
|
37542
38135
|
};
|
|
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) {
|
|
38136
|
+
async function requireGmailDispatchConfirmation(ctx, op, details, fallback) {
|
|
37553
38137
|
const twin = STAGING_TWIN[op];
|
|
37554
38138
|
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
|
|
38139
|
+
return requireDispatchConfirmation(ctx, {
|
|
37556
38140
|
action: op,
|
|
37557
38141
|
message: op === "gmail.filter-forward" ? "Review and confirm this mail-forwarding filter:" : "Review and confirm this email dispatch:",
|
|
37558
38142
|
details,
|
|
37559
38143
|
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
|
-
|
|
38144
|
+
unsupportedNote: note,
|
|
38145
|
+
...fallback ? { fallback: { instruction: CONFIRM_SEND_INSTRUCTION, ...fallback } } : {}
|
|
37561
38146
|
});
|
|
37562
38147
|
}
|
|
37563
38148
|
var EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/gi;
|
|
@@ -37611,10 +38196,6 @@ function logGmailDispatch(tool, recipients, account) {
|
|
|
37611
38196
|
process.stderr.write(`${JSON.stringify(event)}
|
|
37612
38197
|
`);
|
|
37613
38198
|
}
|
|
37614
|
-
function resultText(result) {
|
|
37615
|
-
const first = result.content[0];
|
|
37616
|
-
return first && first.type === "text" && typeof first.text === "string" ? first.text : "{}";
|
|
37617
|
-
}
|
|
37618
38199
|
|
|
37619
38200
|
// src/tools/gmail.ts
|
|
37620
38201
|
var replySchema = {
|
|
@@ -37662,7 +38243,7 @@ function appendReplyFlags(args, f) {
|
|
|
37662
38243
|
if (f.signatureFile) args.push(`--signature-file=${f.signatureFile}`);
|
|
37663
38244
|
args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
|
|
37664
38245
|
}
|
|
37665
|
-
var sendReplySchema = external_exports.object(replySchema);
|
|
38246
|
+
var sendReplySchema = external_exports.object({ ...replySchema, confirmToken: confirmTokenParam });
|
|
37666
38247
|
function parseMetadataHeaders(raw) {
|
|
37667
38248
|
let parsed;
|
|
37668
38249
|
try {
|
|
@@ -37687,12 +38268,69 @@ function computeReplyRecipients(kind, headers, flags) {
|
|
|
37687
38268
|
}
|
|
37688
38269
|
return emails;
|
|
37689
38270
|
}
|
|
37690
|
-
|
|
38271
|
+
function metadataThreadId(raw) {
|
|
38272
|
+
try {
|
|
38273
|
+
const threadId = JSON.parse(raw)?.message?.threadId;
|
|
38274
|
+
return typeof threadId === "string" ? threadId : void 0;
|
|
38275
|
+
} catch {
|
|
38276
|
+
return void 0;
|
|
38277
|
+
}
|
|
38278
|
+
}
|
|
38279
|
+
function replyTokenSubject(kind, messageId, account, headers, threadId, flags) {
|
|
38280
|
+
const removed = new Set(extractEmails(...flags.remove ?? []));
|
|
38281
|
+
const keep = (emails) => emails.filter((e) => !removed.has(e));
|
|
38282
|
+
const to = keep(extractEmails(headers.from, ...kind === "reply-all" ? [headers.to] : [], ...flags.to ?? []));
|
|
38283
|
+
const cc = keep(extractEmails(...kind === "reply-all" ? [headers.cc] : [], ...flags.cc ?? []));
|
|
38284
|
+
const bcc = keep(extractEmails(...flags.bcc ?? []));
|
|
38285
|
+
const attachments = attachmentDetails(flags.attach, flags.attachInline);
|
|
38286
|
+
const subject = flags.subject || (headers.subject ? `Re: ${headers.subject}` : void 0);
|
|
38287
|
+
const from = flags.from ?? (flags.autoFromAddressedAlias ? "the send-as alias the original was addressed to" : senderPreview(account));
|
|
38288
|
+
const localFiles = attachmentDetails([flags.bodyHtmlFile, flags.signatureFile].filter((p) => Boolean(p)), void 0);
|
|
38289
|
+
return {
|
|
38290
|
+
target: messageId,
|
|
38291
|
+
payload: {
|
|
38292
|
+
kind,
|
|
38293
|
+
to,
|
|
38294
|
+
cc,
|
|
38295
|
+
bcc,
|
|
38296
|
+
subject,
|
|
38297
|
+
from,
|
|
38298
|
+
body: flags.body,
|
|
38299
|
+
bodyHtml: flags.bodyHtml,
|
|
38300
|
+
localFiles,
|
|
38301
|
+
signature: Boolean(flags.signature),
|
|
38302
|
+
signatureFrom: flags.signatureFrom,
|
|
38303
|
+
attachments,
|
|
38304
|
+
quote: !flags.noQuote,
|
|
38305
|
+
threadId,
|
|
38306
|
+
inReplyTo: headers.message_id,
|
|
38307
|
+
references: headers.references
|
|
38308
|
+
},
|
|
38309
|
+
preview: {
|
|
38310
|
+
from,
|
|
38311
|
+
to,
|
|
38312
|
+
cc,
|
|
38313
|
+
bcc,
|
|
38314
|
+
subject,
|
|
38315
|
+
body: flags.body,
|
|
38316
|
+
...flags.bodyHtml ? { bodyHtml: flags.bodyHtml } : {},
|
|
38317
|
+
...flags.bodyHtmlFile ? { bodyHtmlFile: flags.bodyHtmlFile } : {},
|
|
38318
|
+
...flags.signature || flags.signatureFrom || flags.signatureFile ? { signature: flags.signatureFile ?? flags.signatureFrom ?? "the Gmail signature of the sending address" } : {},
|
|
38319
|
+
quotesOriginal: !flags.noQuote,
|
|
38320
|
+
attachments: attachmentPreview(attachments),
|
|
38321
|
+
replyingToMessageId: messageId,
|
|
38322
|
+
threadId,
|
|
38323
|
+
inReplyTo: headers.message_id
|
|
38324
|
+
}
|
|
38325
|
+
};
|
|
38326
|
+
}
|
|
38327
|
+
async function sendReply(kind, toolName, messageId, account, flags, ctx, confirmToken) {
|
|
37691
38328
|
const metaResult = await runOrDiagnose(["gmail", "get", pos(messageId), "--format=metadata"], { account });
|
|
37692
38329
|
if (metaResult.isError) return metaResult;
|
|
37693
|
-
const
|
|
38330
|
+
const metaText = resultText(metaResult);
|
|
38331
|
+
const headers = parseMetadataHeaders(metaText);
|
|
37694
38332
|
const recipients = computeReplyRecipients(kind, headers, flags);
|
|
37695
|
-
const confirmation = requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
|
|
38333
|
+
const confirmation = await requireGmailDispatchConfirmation(ctx, replyDispatchOp(kind), {
|
|
37696
38334
|
messageId,
|
|
37697
38335
|
recipients,
|
|
37698
38336
|
recipientCount: recipients.length,
|
|
@@ -37703,6 +38341,13 @@ async function sendReply(kind, toolName, messageId, account, flags, ctx) {
|
|
|
37703
38341
|
bodyHtmlFile: flags.bodyHtmlFile,
|
|
37704
38342
|
attachmentCount: (flags.attach?.length ?? 0) + (flags.attachInline?.length ?? 0),
|
|
37705
38343
|
attachments: attachmentNames(flags.attach, flags.attachInline)
|
|
38344
|
+
}, {
|
|
38345
|
+
tool: toolName,
|
|
38346
|
+
account,
|
|
38347
|
+
confirmToken,
|
|
38348
|
+
// The metadata read above IS the phase-2 re-read: it runs on every call,
|
|
38349
|
+
// so the original's Message-ID/References are fresh here.
|
|
38350
|
+
subject: () => replyTokenSubject(kind, messageId, account, headers, metadataThreadId(metaText), flags)
|
|
37706
38351
|
});
|
|
37707
38352
|
if (confirmation) return confirmation;
|
|
37708
38353
|
const args = ["gmail", kind, pos(messageId)];
|
|
@@ -37720,7 +38365,7 @@ function vetGmailRun(subcommand, args) {
|
|
|
37720
38365
|
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
38366
|
}
|
|
37722
38367
|
if (subcommand === "settings") {
|
|
37723
|
-
const blocked = args
|
|
38368
|
+
const blocked = hasCommandWord(args, GMAIL_RUN_BLOCKED_SETTINGS);
|
|
37724
38369
|
if (blocked) {
|
|
37725
38370
|
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
38371
|
}
|
|
@@ -37779,7 +38424,7 @@ function registerGmailTools(server) {
|
|
|
37779
38424
|
return runOrDiagnose(args, { account });
|
|
37780
38425
|
});
|
|
37781
38426
|
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.',
|
|
38427
|
+
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
38428
|
annotations: { destructiveHint: true },
|
|
37784
38429
|
inputSchema: external_exports.object({
|
|
37785
38430
|
to: external_exports.string().describe("Recipient(s), comma-separated"),
|
|
@@ -37792,9 +38437,10 @@ function registerGmailTools(server) {
|
|
|
37792
38437
|
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
38438
|
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
38439
|
attachInline: attachInlineParam,
|
|
37795
|
-
account: accountParam
|
|
38440
|
+
account: accountParam,
|
|
38441
|
+
confirmToken: confirmTokenParam
|
|
37796
38442
|
})
|
|
37797
|
-
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account }, ctx) => {
|
|
38443
|
+
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account, confirmToken }, ctx) => {
|
|
37798
38444
|
confinePaths(attach, "attach");
|
|
37799
38445
|
const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
|
|
37800
38446
|
if (cc) args.push(`--cc=${cc}`);
|
|
@@ -37806,7 +38452,7 @@ function registerGmailTools(server) {
|
|
|
37806
38452
|
const inline = inlineAttachmentArgs("attach", attachInline, args);
|
|
37807
38453
|
args.push(...inline);
|
|
37808
38454
|
const recipients = extractEmails(to, cc, bcc);
|
|
37809
|
-
const confirmation = requireGmailDispatchConfirmation(ctx, "gmail.send", {
|
|
38455
|
+
const confirmation = await requireGmailDispatchConfirmation(ctx, "gmail.send", {
|
|
37810
38456
|
to,
|
|
37811
38457
|
cc,
|
|
37812
38458
|
bcc,
|
|
@@ -37819,6 +38465,32 @@ function registerGmailTools(server) {
|
|
|
37819
38465
|
quoting: Boolean(quote),
|
|
37820
38466
|
attachmentCount: (attach?.length ?? 0) + (attachInline?.length ?? 0),
|
|
37821
38467
|
attachments: attachmentNames(attach, attachInline)
|
|
38468
|
+
}, {
|
|
38469
|
+
tool: "gog_gmail_send",
|
|
38470
|
+
account,
|
|
38471
|
+
confirmToken,
|
|
38472
|
+
// Nothing is stored between the phases: the payload IS the arguments,
|
|
38473
|
+
// so phase 2 must repeat them and any difference is a changed send.
|
|
38474
|
+
subject: () => {
|
|
38475
|
+
const attachments = attachmentDetails(attach, attachInline);
|
|
38476
|
+
const from = senderPreview(account);
|
|
38477
|
+
return {
|
|
38478
|
+
target: replyToMessageId ?? threadId ?? "",
|
|
38479
|
+
payload: { from, to, cc, bcc, subject, body, attachments, threadId, inReplyTo: replyToMessageId, quote: Boolean(quote) },
|
|
38480
|
+
preview: {
|
|
38481
|
+
from,
|
|
38482
|
+
to,
|
|
38483
|
+
cc,
|
|
38484
|
+
bcc,
|
|
38485
|
+
subject,
|
|
38486
|
+
body,
|
|
38487
|
+
attachments: attachmentPreview(attachments),
|
|
38488
|
+
threadId,
|
|
38489
|
+
inReplyTo: replyToMessageId,
|
|
38490
|
+
quotesOriginal: Boolean(quote)
|
|
38491
|
+
}
|
|
38492
|
+
};
|
|
38493
|
+
}
|
|
37822
38494
|
});
|
|
37823
38495
|
if (confirmation) return confirmation;
|
|
37824
38496
|
const result = await runOrDiagnose(args, { account });
|
|
@@ -37826,22 +38498,22 @@ function registerGmailTools(server) {
|
|
|
37826
38498
|
return result;
|
|
37827
38499
|
});
|
|
37828
38500
|
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.',
|
|
38501
|
+
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
38502
|
annotations: { destructiveHint: true },
|
|
37831
38503
|
inputSchema: sendReplySchema
|
|
37832
|
-
}, async ({ messageId, account, ...flags }, ctx) => {
|
|
38504
|
+
}, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
|
|
37833
38505
|
assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
|
|
37834
38506
|
confineReplyPaths(flags);
|
|
37835
|
-
return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx);
|
|
38507
|
+
return sendReply("reply", "gog_gmail_reply", messageId, account, flags, ctx, confirmToken);
|
|
37836
38508
|
});
|
|
37837
38509
|
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.',
|
|
38510
|
+
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
38511
|
annotations: { destructiveHint: true },
|
|
37840
38512
|
inputSchema: sendReplySchema
|
|
37841
|
-
}, async ({ messageId, account, ...flags }, ctx) => {
|
|
38513
|
+
}, async ({ messageId, account, confirmToken, ...flags }, ctx) => {
|
|
37842
38514
|
assertNotBoth("bodyHtml", "bodyHtmlFile", flags.bodyHtml, flags.bodyHtmlFile);
|
|
37843
38515
|
confineReplyPaths(flags);
|
|
37844
|
-
return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx);
|
|
38516
|
+
return sendReply("reply-all", "gog_gmail_reply_all", messageId, account, flags, ctx, confirmToken);
|
|
37845
38517
|
});
|
|
37846
38518
|
registerRunTool(server, {
|
|
37847
38519
|
service: "gmail",
|
|
@@ -38182,7 +38854,7 @@ function registerTasksTools(server) {
|
|
|
38182
38854
|
}
|
|
38183
38855
|
|
|
38184
38856
|
// src/server.ts
|
|
38185
|
-
var VERSION = true ? "4.
|
|
38857
|
+
var VERSION = true ? "4.5.0" : "0.0.0";
|
|
38186
38858
|
var BASE_TOOL_REGISTRARS = [
|
|
38187
38859
|
registerApiTools,
|
|
38188
38860
|
registerAppScriptTools,
|
|
@@ -38234,12 +38906,12 @@ async function bootstrapGogAuth(env = process.env, options = {}) {
|
|
|
38234
38906
|
const [clientId, clientSecret, refreshToken, account] = values;
|
|
38235
38907
|
const { spawner } = options;
|
|
38236
38908
|
try {
|
|
38237
|
-
const { createHash:
|
|
38909
|
+
const { createHash: createHash5 } = await import("node:crypto");
|
|
38238
38910
|
const { mkdir, readFile, writeFile, chmod } = await import("node:fs/promises");
|
|
38239
38911
|
const home = options.home ?? (await import("node:os")).homedir();
|
|
38240
38912
|
const dir = join5(home, ".gogcli-mcp");
|
|
38241
38913
|
const marker = join5(dir, AUTH_BOOTSTRAP_MARKER);
|
|
38242
|
-
const fingerprint =
|
|
38914
|
+
const fingerprint = createHash5("sha256").update(values.join("\0")).digest("hex");
|
|
38243
38915
|
const previous = await readFile(marker, "utf8").catch(() => void 0);
|
|
38244
38916
|
if (previous === fingerprint && await accountListed(account, spawner)) return "present";
|
|
38245
38917
|
await run(
|