gogcli-mcp 2.24.0 → 2.25.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 +122 -13
- package/dist/lib.js +130 -13
- package/manifest.json +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/attachments.ts +263 -0
- package/src/lib.ts +14 -0
- package/src/runner.ts +169 -14
- package/src/tools/gmail.ts +17 -3
- package/src/worker.ts +1 -1
- package/tests/attachments.test.ts +227 -0
- package/tests/runner-file-args-failure.test.ts +48 -3
- package/tests/runner.test.ts +126 -0
- package/tests/tools/gmail.test.ts +89 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.25.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "gogcli",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
18
|
-
"version": "2.
|
|
18
|
+
"version": "2.25.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/index.js
CHANGED
|
@@ -31177,10 +31177,11 @@ function sanitizedEnv() {
|
|
|
31177
31177
|
}
|
|
31178
31178
|
return result;
|
|
31179
31179
|
}
|
|
31180
|
+
var TOKEN_LEFT_BOUNDARY = "(?<![A-Za-z0-9+/])";
|
|
31180
31181
|
var GOOGLE_TOKEN_PATTERNS = [
|
|
31181
|
-
|
|
31182
|
+
new RegExp(`${TOKEN_LEFT_BOUNDARY}ya29\\.[A-Za-z0-9._\\-]+`, "g"),
|
|
31182
31183
|
// OAuth2 access tokens
|
|
31183
|
-
|
|
31184
|
+
new RegExp(`${TOKEN_LEFT_BOUNDARY}1//[A-Za-z0-9._\\-]+`, "g")
|
|
31184
31185
|
// OAuth2 refresh tokens
|
|
31185
31186
|
];
|
|
31186
31187
|
function redactGoogleTokens(text) {
|
|
@@ -31193,6 +31194,26 @@ function redactGoogleTokens(text) {
|
|
|
31193
31194
|
function redactSecrets2(text) {
|
|
31194
31195
|
return redactGoogleTokens(redactSecrets(text));
|
|
31195
31196
|
}
|
|
31197
|
+
var OPAQUE_FIELD_VALUE = "[A-Za-z0-9+/_-]{16,}={0,2}";
|
|
31198
|
+
var opaquePlaceholder = (i) => `\0gogOpaque${i}\0`;
|
|
31199
|
+
function redactPreservingOpaqueFields(text, fields, redact) {
|
|
31200
|
+
const lifted = [];
|
|
31201
|
+
let staged = text;
|
|
31202
|
+
for (const field of fields) {
|
|
31203
|
+
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31204
|
+
const re = new RegExp(`("${escaped}"\\s*:\\s*")(${OPAQUE_FIELD_VALUE})(")`, "g");
|
|
31205
|
+
staged = staged.replace(re, (_m, open, value, close) => {
|
|
31206
|
+
lifted.push(value);
|
|
31207
|
+
return `${open}${opaquePlaceholder(lifted.length - 1)}${close}`;
|
|
31208
|
+
});
|
|
31209
|
+
}
|
|
31210
|
+
if (lifted.length === 0) return redact(text);
|
|
31211
|
+
let redacted = redact(staged);
|
|
31212
|
+
lifted.forEach((value, i) => {
|
|
31213
|
+
redacted = redacted.split(opaquePlaceholder(i)).join(value);
|
|
31214
|
+
});
|
|
31215
|
+
return redacted;
|
|
31216
|
+
}
|
|
31196
31217
|
function augmentedPath() {
|
|
31197
31218
|
const home = process.env.HOME;
|
|
31198
31219
|
const candidates = [
|
|
@@ -31223,19 +31244,24 @@ function formatTimeout(ms) {
|
|
|
31223
31244
|
return `${ms}ms`;
|
|
31224
31245
|
}
|
|
31225
31246
|
async function spawnWithTempFiles(args, opts) {
|
|
31226
|
-
const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
|
|
31247
|
+
const { mkdtemp, mkdir, writeFile, rm } = await import("node:fs/promises");
|
|
31227
31248
|
const { tmpdir } = await import("node:os");
|
|
31228
31249
|
const dir = await mkdtemp(join(tmpdir(), "gogcli-mcp-"));
|
|
31229
31250
|
try {
|
|
31230
31251
|
const argv = [];
|
|
31252
|
+
let seq = 0;
|
|
31231
31253
|
for (const arg of args) {
|
|
31232
31254
|
if (!isGogFileArg(arg)) {
|
|
31233
31255
|
argv.push(arg);
|
|
31234
31256
|
continue;
|
|
31235
31257
|
}
|
|
31236
|
-
const
|
|
31237
|
-
|
|
31238
|
-
|
|
31258
|
+
const sub = join(dir, String(seq));
|
|
31259
|
+
seq += 1;
|
|
31260
|
+
await mkdir(sub, { recursive: true, mode: 448 });
|
|
31261
|
+
const path = join(sub, arg.filename ?? `${arg.flag}.${arg.ext ?? "txt"}`);
|
|
31262
|
+
const data = arg.encoding === "base64" ? Buffer.from(arg.contents, "base64") : Buffer.from(arg.contents, "utf8");
|
|
31263
|
+
await writeFile(path, data, { mode: 384 });
|
|
31264
|
+
argv.push(arg.positional ? path : `--${arg.flag}=${path}`);
|
|
31239
31265
|
}
|
|
31240
31266
|
return await spawnGog(argv, opts);
|
|
31241
31267
|
} finally {
|
|
@@ -31320,8 +31346,9 @@ function assembleArgs(args, opts) {
|
|
|
31320
31346
|
return fullArgs;
|
|
31321
31347
|
}
|
|
31322
31348
|
async function run(args, options = {}) {
|
|
31323
|
-
const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full" } = options;
|
|
31324
|
-
const
|
|
31349
|
+
const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full", opaqueFields } = options;
|
|
31350
|
+
const base = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
|
|
31351
|
+
const redact = opaqueFields?.length ? (text) => redactPreservingOpaqueFields(text, opaqueFields, base) : base;
|
|
31325
31352
|
const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
|
|
31326
31353
|
const store = activeExecutor();
|
|
31327
31354
|
try {
|
|
@@ -31335,7 +31362,7 @@ async function run(args, options = {}) {
|
|
|
31335
31362
|
}
|
|
31336
31363
|
return redact(output);
|
|
31337
31364
|
} catch (err) {
|
|
31338
|
-
const message =
|
|
31365
|
+
const message = base(err instanceof Error ? err.message : String(err));
|
|
31339
31366
|
if (isRunnerTransportError(err)) {
|
|
31340
31367
|
throw new RunnerTransportError(message, err.kind, err.status);
|
|
31341
31368
|
}
|
|
@@ -32950,6 +32977,85 @@ function finish(base, itemsKey, merged, token) {
|
|
|
32950
32977
|
return rawTextResult(JSON.stringify(out));
|
|
32951
32978
|
}
|
|
32952
32979
|
|
|
32980
|
+
// src/attachments.ts
|
|
32981
|
+
var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
|
|
32982
|
+
var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
32983
|
+
var RUNNER_BODY_JSON_RESERVE_BYTES = 256 * 1024;
|
|
32984
|
+
var MAX_REQUEST_PAYLOAD_WIRE_BYTES = RUNNER_MAX_BODY_BYTES - RUNNER_BODY_JSON_RESERVE_BYTES;
|
|
32985
|
+
var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
|
|
32986
|
+
function wireBytesOf(arg) {
|
|
32987
|
+
if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
|
|
32988
|
+
return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
|
|
32989
|
+
}
|
|
32990
|
+
var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
|
|
32991
|
+
var INLINE_ATTACHMENT_LIMITS_TEXT = `up to ${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)} per file and ${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)} in total`;
|
|
32992
|
+
var inlineAttachmentSchema = external_exports.object({
|
|
32993
|
+
filename: external_exports.string().min(1).describe(
|
|
32994
|
+
`Filename the recipient will see, e.g. "pendant-layouts.png". gog infers the attachment's MIME type from this extension, so give it the right one \u2014 a .png sent as "layouts" arrives as an untyped blob. Must be a single filename, not a path.`
|
|
32995
|
+
),
|
|
32996
|
+
contentBase64: external_exports.string().min(1).describe(
|
|
32997
|
+
"The file's bytes, base64-encoded (standard alphabet, with padding). This is the whole point of this parameter: the bytes travel with the request, so nothing needs to exist on the gog server's filesystem."
|
|
32998
|
+
)
|
|
32999
|
+
});
|
|
33000
|
+
var attachInlineParam = external_exports.array(inlineAttachmentSchema).optional().describe(
|
|
33001
|
+
`Attachments supplied as BYTES rather than as server-side paths \u2014 use this whenever you hold a file and the gog server does not, which is always the case on the hosted connector and on any remote deployment. Each entry is {filename, contentBase64} (${INLINE_ATTACHMENT_LIMITS_TEXT}). Can be combined with \`attach\`: the two name disjoint files (paths read on the server vs. bytes sent with the call), and both end up as ordinary attachments on the message.`
|
|
33002
|
+
);
|
|
33003
|
+
function validateFilename(filename, where) {
|
|
33004
|
+
if (/[/\\]/.test(filename)) {
|
|
33005
|
+
throw new Error(
|
|
33006
|
+
`${where}: filename ${JSON.stringify(filename)} must be a bare filename, not a path. Pass just the name the recipient should see, e.g. "report.pdf".`
|
|
33007
|
+
);
|
|
33008
|
+
}
|
|
33009
|
+
if (/[\x00-\x1f]/.test(filename) || /^\.+$/.test(filename) || filename.length > 200) {
|
|
33010
|
+
throw new Error(
|
|
33011
|
+
`${where}: filename ${JSON.stringify(filename)} is not a usable filename (no control characters, not "."/"..", 200 characters max).`
|
|
33012
|
+
);
|
|
33013
|
+
}
|
|
33014
|
+
}
|
|
33015
|
+
function decodedLength(contentBase64) {
|
|
33016
|
+
const buf = Buffer.from(contentBase64, "base64");
|
|
33017
|
+
return buf.toString("base64") === contentBase64 ? buf.length : null;
|
|
33018
|
+
}
|
|
33019
|
+
function inlineFileArg(flag, attachment, opts = {}) {
|
|
33020
|
+
const { filename, contentBase64 } = attachment;
|
|
33021
|
+
const where = opts.where ?? `attachInline entry ${JSON.stringify(filename)}`;
|
|
33022
|
+
validateFilename(filename, where);
|
|
33023
|
+
const bytes = decodedLength(contentBase64);
|
|
33024
|
+
if (bytes === null) {
|
|
33025
|
+
throw new Error(
|
|
33026
|
+
`${where}: contents are not valid base64. Send the standard alphabet with padding and no line breaks \u2014 the value must survive a decode/re-encode round trip unchanged.`
|
|
33027
|
+
);
|
|
33028
|
+
}
|
|
33029
|
+
if (bytes > MAX_INLINE_ATTACHMENT_BYTES) {
|
|
33030
|
+
throw new Error(
|
|
33031
|
+
`${where}: ${bytes} bytes exceeds the ${MAX_INLINE_ATTACHMENT_BYTES}-byte (${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)}) per-file limit for inline content. Upload it to Drive and link to it instead, or send it from a local (stdio) deployment using a real server-side path.`
|
|
33032
|
+
);
|
|
33033
|
+
}
|
|
33034
|
+
const arg = { kind: "file", flag, contents: contentBase64, encoding: "base64", filename };
|
|
33035
|
+
if (opts.positional) arg.positional = true;
|
|
33036
|
+
return { arg, bytes };
|
|
33037
|
+
}
|
|
33038
|
+
function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
|
|
33039
|
+
if (!attachments?.length) return [];
|
|
33040
|
+
const args = [];
|
|
33041
|
+
const siblingWire = siblingArgs.reduce((sum, arg) => sum + wireBytesOf(arg), 0);
|
|
33042
|
+
let attachmentWire = 0;
|
|
33043
|
+
let decodedTotal = 0;
|
|
33044
|
+
for (const attachment of attachments) {
|
|
33045
|
+
const { arg, bytes } = inlineFileArg(flag, attachment);
|
|
33046
|
+
attachmentWire += arg.contents.length;
|
|
33047
|
+
decodedTotal += bytes;
|
|
33048
|
+
if (siblingWire + attachmentWire > MAX_REQUEST_PAYLOAD_WIRE_BYTES) {
|
|
33049
|
+
const blame = attachmentWire <= MAX_REQUEST_PAYLOAD_WIRE_BYTES ? ` These attachments would fit on their own; the rest of the message (its body, mostly) spends ${siblingWire} bytes of the same budget.` : "";
|
|
33050
|
+
throw new Error(
|
|
33051
|
+
`This message is too large to send: ${decodedTotal} bytes of attachments (${attachmentWire} bytes once base64-encoded for transit) exceed the ${MAX_REQUEST_PAYLOAD_WIRE_BYTES}-byte request limit.${blame} The ceiling for attachments alone is ${MAX_INLINE_ATTACHMENT_TOTAL_BYTES} bytes (${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)}); a long body lowers it. Send fewer or smaller files per message, shorten the body, or upload the large files to Drive and link them.`
|
|
33052
|
+
);
|
|
33053
|
+
}
|
|
33054
|
+
args.push(arg);
|
|
33055
|
+
}
|
|
33056
|
+
return args;
|
|
33057
|
+
}
|
|
33058
|
+
|
|
32953
33059
|
// src/tools/gmail.ts
|
|
32954
33060
|
function registerGmailTools(server) {
|
|
32955
33061
|
server.registerTool("gog_gmail_search", {
|
|
@@ -33003,7 +33109,7 @@ function registerGmailTools(server) {
|
|
|
33003
33109
|
return runOrDiagnose(args, { account });
|
|
33004
33110
|
});
|
|
33005
33111
|
server.registerTool("gog_gmail_send", {
|
|
33006
|
-
description:
|
|
33112
|
+
description: 'Send an email. 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.',
|
|
33007
33113
|
annotations: { destructiveHint: true },
|
|
33008
33114
|
inputSchema: {
|
|
33009
33115
|
to: external_exports.string().describe("Recipient(s), comma-separated"),
|
|
@@ -33013,16 +33119,19 @@ function registerGmailTools(server) {
|
|
|
33013
33119
|
bcc: external_exports.string().optional().describe("BCC recipients, comma-separated"),
|
|
33014
33120
|
replyToMessageId: external_exports.string().optional().describe("Message ID to reply to"),
|
|
33015
33121
|
threadId: external_exports.string().optional().describe("Thread ID to reply within"),
|
|
33016
|
-
attach: external_exports.array(external_exports.string()).optional().describe(
|
|
33122
|
+
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 the hosted connector or any GOG_RUNNER_URL backend 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.`),
|
|
33123
|
+
attachInline: attachInlineParam,
|
|
33017
33124
|
account: accountParam
|
|
33018
33125
|
}
|
|
33019
|
-
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, account }) => {
|
|
33126
|
+
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, attachInline, account }) => {
|
|
33020
33127
|
const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
|
|
33021
33128
|
if (cc) args.push(`--cc=${cc}`);
|
|
33022
33129
|
if (bcc) args.push(`--bcc=${bcc}`);
|
|
33023
33130
|
if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
|
|
33024
33131
|
if (threadId) args.push(`--thread-id=${threadId}`);
|
|
33025
33132
|
if (attach) for (const path of attach) args.push(`--attach=${path}`);
|
|
33133
|
+
const inline = inlineAttachmentArgs("attach", attachInline, args);
|
|
33134
|
+
args.push(...inline);
|
|
33026
33135
|
return runOrDiagnose(args, { account });
|
|
33027
33136
|
});
|
|
33028
33137
|
registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
|
|
@@ -33352,7 +33461,7 @@ function registerTasksTools(server) {
|
|
|
33352
33461
|
}
|
|
33353
33462
|
|
|
33354
33463
|
// src/server.ts
|
|
33355
|
-
var VERSION = true ? "2.
|
|
33464
|
+
var VERSION = true ? "2.25.0" : "0.0.0";
|
|
33356
33465
|
var BASE_TOOL_REGISTRARS = [
|
|
33357
33466
|
registerApiTools,
|
|
33358
33467
|
registerAuthTools,
|
package/dist/lib.js
CHANGED
|
@@ -23056,10 +23056,11 @@ function sanitizedEnv() {
|
|
|
23056
23056
|
}
|
|
23057
23057
|
return result;
|
|
23058
23058
|
}
|
|
23059
|
+
var TOKEN_LEFT_BOUNDARY = "(?<![A-Za-z0-9+/])";
|
|
23059
23060
|
var GOOGLE_TOKEN_PATTERNS = [
|
|
23060
|
-
|
|
23061
|
+
new RegExp(`${TOKEN_LEFT_BOUNDARY}ya29\\.[A-Za-z0-9._\\-]+`, "g"),
|
|
23061
23062
|
// OAuth2 access tokens
|
|
23062
|
-
|
|
23063
|
+
new RegExp(`${TOKEN_LEFT_BOUNDARY}1//[A-Za-z0-9._\\-]+`, "g")
|
|
23063
23064
|
// OAuth2 refresh tokens
|
|
23064
23065
|
];
|
|
23065
23066
|
function redactGoogleTokens(text) {
|
|
@@ -23072,6 +23073,26 @@ function redactGoogleTokens(text) {
|
|
|
23072
23073
|
function redactSecrets2(text) {
|
|
23073
23074
|
return redactGoogleTokens(redactSecrets(text));
|
|
23074
23075
|
}
|
|
23076
|
+
var OPAQUE_FIELD_VALUE = "[A-Za-z0-9+/_-]{16,}={0,2}";
|
|
23077
|
+
var opaquePlaceholder = (i) => `\0gogOpaque${i}\0`;
|
|
23078
|
+
function redactPreservingOpaqueFields(text, fields, redact) {
|
|
23079
|
+
const lifted = [];
|
|
23080
|
+
let staged = text;
|
|
23081
|
+
for (const field of fields) {
|
|
23082
|
+
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23083
|
+
const re = new RegExp(`("${escaped}"\\s*:\\s*")(${OPAQUE_FIELD_VALUE})(")`, "g");
|
|
23084
|
+
staged = staged.replace(re, (_m, open, value, close) => {
|
|
23085
|
+
lifted.push(value);
|
|
23086
|
+
return `${open}${opaquePlaceholder(lifted.length - 1)}${close}`;
|
|
23087
|
+
});
|
|
23088
|
+
}
|
|
23089
|
+
if (lifted.length === 0) return redact(text);
|
|
23090
|
+
let redacted = redact(staged);
|
|
23091
|
+
lifted.forEach((value, i) => {
|
|
23092
|
+
redacted = redacted.split(opaquePlaceholder(i)).join(value);
|
|
23093
|
+
});
|
|
23094
|
+
return redacted;
|
|
23095
|
+
}
|
|
23075
23096
|
function augmentedPath() {
|
|
23076
23097
|
const home = process.env.HOME;
|
|
23077
23098
|
const candidates = [
|
|
@@ -23102,19 +23123,24 @@ function formatTimeout(ms) {
|
|
|
23102
23123
|
return `${ms}ms`;
|
|
23103
23124
|
}
|
|
23104
23125
|
async function spawnWithTempFiles(args, opts) {
|
|
23105
|
-
const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
|
|
23126
|
+
const { mkdtemp, mkdir, writeFile, rm } = await import("node:fs/promises");
|
|
23106
23127
|
const { tmpdir } = await import("node:os");
|
|
23107
23128
|
const dir = await mkdtemp(join(tmpdir(), "gogcli-mcp-"));
|
|
23108
23129
|
try {
|
|
23109
23130
|
const argv = [];
|
|
23131
|
+
let seq = 0;
|
|
23110
23132
|
for (const arg of args) {
|
|
23111
23133
|
if (!isGogFileArg(arg)) {
|
|
23112
23134
|
argv.push(arg);
|
|
23113
23135
|
continue;
|
|
23114
23136
|
}
|
|
23115
|
-
const
|
|
23116
|
-
|
|
23117
|
-
|
|
23137
|
+
const sub = join(dir, String(seq));
|
|
23138
|
+
seq += 1;
|
|
23139
|
+
await mkdir(sub, { recursive: true, mode: 448 });
|
|
23140
|
+
const path = join(sub, arg.filename ?? `${arg.flag}.${arg.ext ?? "txt"}`);
|
|
23141
|
+
const data = arg.encoding === "base64" ? Buffer.from(arg.contents, "base64") : Buffer.from(arg.contents, "utf8");
|
|
23142
|
+
await writeFile(path, data, { mode: 384 });
|
|
23143
|
+
argv.push(arg.positional ? path : `--${arg.flag}=${path}`);
|
|
23118
23144
|
}
|
|
23119
23145
|
return await spawnGog(argv, opts);
|
|
23120
23146
|
} finally {
|
|
@@ -23199,8 +23225,9 @@ function assembleArgs(args, opts) {
|
|
|
23199
23225
|
return fullArgs;
|
|
23200
23226
|
}
|
|
23201
23227
|
async function run(args, options = {}) {
|
|
23202
|
-
const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full" } = options;
|
|
23203
|
-
const
|
|
23228
|
+
const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full", opaqueFields } = options;
|
|
23229
|
+
const base = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
|
|
23230
|
+
const redact = opaqueFields?.length ? (text) => redactPreservingOpaqueFields(text, opaqueFields, base) : base;
|
|
23204
23231
|
const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
|
|
23205
23232
|
const store = activeExecutor();
|
|
23206
23233
|
try {
|
|
@@ -23214,7 +23241,7 @@ async function run(args, options = {}) {
|
|
|
23214
23241
|
}
|
|
23215
23242
|
return redact(output);
|
|
23216
23243
|
} catch (err) {
|
|
23217
|
-
const message =
|
|
23244
|
+
const message = base(err instanceof Error ? err.message : String(err));
|
|
23218
23245
|
if (isRunnerTransportError(err)) {
|
|
23219
23246
|
throw new RunnerTransportError(message, err.kind, err.status);
|
|
23220
23247
|
}
|
|
@@ -24838,6 +24865,85 @@ function finish(base, itemsKey, merged, token) {
|
|
|
24838
24865
|
return rawTextResult(JSON.stringify(out));
|
|
24839
24866
|
}
|
|
24840
24867
|
|
|
24868
|
+
// src/attachments.ts
|
|
24869
|
+
var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
|
|
24870
|
+
var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
24871
|
+
var RUNNER_BODY_JSON_RESERVE_BYTES = 256 * 1024;
|
|
24872
|
+
var MAX_REQUEST_PAYLOAD_WIRE_BYTES = RUNNER_MAX_BODY_BYTES - RUNNER_BODY_JSON_RESERVE_BYTES;
|
|
24873
|
+
var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
|
|
24874
|
+
function wireBytesOf(arg) {
|
|
24875
|
+
if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
|
|
24876
|
+
return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
|
|
24877
|
+
}
|
|
24878
|
+
var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
|
|
24879
|
+
var INLINE_ATTACHMENT_LIMITS_TEXT = `up to ${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)} per file and ${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)} in total`;
|
|
24880
|
+
var inlineAttachmentSchema = external_exports.object({
|
|
24881
|
+
filename: external_exports.string().min(1).describe(
|
|
24882
|
+
`Filename the recipient will see, e.g. "pendant-layouts.png". gog infers the attachment's MIME type from this extension, so give it the right one \u2014 a .png sent as "layouts" arrives as an untyped blob. Must be a single filename, not a path.`
|
|
24883
|
+
),
|
|
24884
|
+
contentBase64: external_exports.string().min(1).describe(
|
|
24885
|
+
"The file's bytes, base64-encoded (standard alphabet, with padding). This is the whole point of this parameter: the bytes travel with the request, so nothing needs to exist on the gog server's filesystem."
|
|
24886
|
+
)
|
|
24887
|
+
});
|
|
24888
|
+
var attachInlineParam = external_exports.array(inlineAttachmentSchema).optional().describe(
|
|
24889
|
+
`Attachments supplied as BYTES rather than as server-side paths \u2014 use this whenever you hold a file and the gog server does not, which is always the case on the hosted connector and on any remote deployment. Each entry is {filename, contentBase64} (${INLINE_ATTACHMENT_LIMITS_TEXT}). Can be combined with \`attach\`: the two name disjoint files (paths read on the server vs. bytes sent with the call), and both end up as ordinary attachments on the message.`
|
|
24890
|
+
);
|
|
24891
|
+
function validateFilename(filename, where) {
|
|
24892
|
+
if (/[/\\]/.test(filename)) {
|
|
24893
|
+
throw new Error(
|
|
24894
|
+
`${where}: filename ${JSON.stringify(filename)} must be a bare filename, not a path. Pass just the name the recipient should see, e.g. "report.pdf".`
|
|
24895
|
+
);
|
|
24896
|
+
}
|
|
24897
|
+
if (/[\x00-\x1f]/.test(filename) || /^\.+$/.test(filename) || filename.length > 200) {
|
|
24898
|
+
throw new Error(
|
|
24899
|
+
`${where}: filename ${JSON.stringify(filename)} is not a usable filename (no control characters, not "."/"..", 200 characters max).`
|
|
24900
|
+
);
|
|
24901
|
+
}
|
|
24902
|
+
}
|
|
24903
|
+
function decodedLength(contentBase64) {
|
|
24904
|
+
const buf = Buffer.from(contentBase64, "base64");
|
|
24905
|
+
return buf.toString("base64") === contentBase64 ? buf.length : null;
|
|
24906
|
+
}
|
|
24907
|
+
function inlineFileArg(flag, attachment, opts = {}) {
|
|
24908
|
+
const { filename, contentBase64 } = attachment;
|
|
24909
|
+
const where = opts.where ?? `attachInline entry ${JSON.stringify(filename)}`;
|
|
24910
|
+
validateFilename(filename, where);
|
|
24911
|
+
const bytes = decodedLength(contentBase64);
|
|
24912
|
+
if (bytes === null) {
|
|
24913
|
+
throw new Error(
|
|
24914
|
+
`${where}: contents are not valid base64. Send the standard alphabet with padding and no line breaks \u2014 the value must survive a decode/re-encode round trip unchanged.`
|
|
24915
|
+
);
|
|
24916
|
+
}
|
|
24917
|
+
if (bytes > MAX_INLINE_ATTACHMENT_BYTES) {
|
|
24918
|
+
throw new Error(
|
|
24919
|
+
`${where}: ${bytes} bytes exceeds the ${MAX_INLINE_ATTACHMENT_BYTES}-byte (${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)}) per-file limit for inline content. Upload it to Drive and link to it instead, or send it from a local (stdio) deployment using a real server-side path.`
|
|
24920
|
+
);
|
|
24921
|
+
}
|
|
24922
|
+
const arg = { kind: "file", flag, contents: contentBase64, encoding: "base64", filename };
|
|
24923
|
+
if (opts.positional) arg.positional = true;
|
|
24924
|
+
return { arg, bytes };
|
|
24925
|
+
}
|
|
24926
|
+
function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
|
|
24927
|
+
if (!attachments?.length) return [];
|
|
24928
|
+
const args = [];
|
|
24929
|
+
const siblingWire = siblingArgs.reduce((sum, arg) => sum + wireBytesOf(arg), 0);
|
|
24930
|
+
let attachmentWire = 0;
|
|
24931
|
+
let decodedTotal = 0;
|
|
24932
|
+
for (const attachment of attachments) {
|
|
24933
|
+
const { arg, bytes } = inlineFileArg(flag, attachment);
|
|
24934
|
+
attachmentWire += arg.contents.length;
|
|
24935
|
+
decodedTotal += bytes;
|
|
24936
|
+
if (siblingWire + attachmentWire > MAX_REQUEST_PAYLOAD_WIRE_BYTES) {
|
|
24937
|
+
const blame = attachmentWire <= MAX_REQUEST_PAYLOAD_WIRE_BYTES ? ` These attachments would fit on their own; the rest of the message (its body, mostly) spends ${siblingWire} bytes of the same budget.` : "";
|
|
24938
|
+
throw new Error(
|
|
24939
|
+
`This message is too large to send: ${decodedTotal} bytes of attachments (${attachmentWire} bytes once base64-encoded for transit) exceed the ${MAX_REQUEST_PAYLOAD_WIRE_BYTES}-byte request limit.${blame} The ceiling for attachments alone is ${MAX_INLINE_ATTACHMENT_TOTAL_BYTES} bytes (${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)}); a long body lowers it. Send fewer or smaller files per message, shorten the body, or upload the large files to Drive and link them.`
|
|
24940
|
+
);
|
|
24941
|
+
}
|
|
24942
|
+
args.push(arg);
|
|
24943
|
+
}
|
|
24944
|
+
return args;
|
|
24945
|
+
}
|
|
24946
|
+
|
|
24841
24947
|
// src/tools/gmail.ts
|
|
24842
24948
|
function registerGmailTools(server) {
|
|
24843
24949
|
server.registerTool("gog_gmail_search", {
|
|
@@ -24891,7 +24997,7 @@ function registerGmailTools(server) {
|
|
|
24891
24997
|
return runOrDiagnose(args, { account });
|
|
24892
24998
|
});
|
|
24893
24999
|
server.registerTool("gog_gmail_send", {
|
|
24894
|
-
description:
|
|
25000
|
+
description: 'Send an email. 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.',
|
|
24895
25001
|
annotations: { destructiveHint: true },
|
|
24896
25002
|
inputSchema: {
|
|
24897
25003
|
to: external_exports.string().describe("Recipient(s), comma-separated"),
|
|
@@ -24901,16 +25007,19 @@ function registerGmailTools(server) {
|
|
|
24901
25007
|
bcc: external_exports.string().optional().describe("BCC recipients, comma-separated"),
|
|
24902
25008
|
replyToMessageId: external_exports.string().optional().describe("Message ID to reply to"),
|
|
24903
25009
|
threadId: external_exports.string().optional().describe("Thread ID to reply within"),
|
|
24904
|
-
attach: external_exports.array(external_exports.string()).optional().describe(
|
|
25010
|
+
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 the hosted connector or any GOG_RUNNER_URL backend 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.`),
|
|
25011
|
+
attachInline: attachInlineParam,
|
|
24905
25012
|
account: accountParam
|
|
24906
25013
|
}
|
|
24907
|
-
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, account }) => {
|
|
25014
|
+
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, attachInline, account }) => {
|
|
24908
25015
|
const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
|
|
24909
25016
|
if (cc) args.push(`--cc=${cc}`);
|
|
24910
25017
|
if (bcc) args.push(`--bcc=${bcc}`);
|
|
24911
25018
|
if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
|
|
24912
25019
|
if (threadId) args.push(`--thread-id=${threadId}`);
|
|
24913
25020
|
if (attach) for (const path of attach) args.push(`--attach=${path}`);
|
|
25021
|
+
const inline = inlineAttachmentArgs("attach", attachInline, args);
|
|
25022
|
+
args.push(...inline);
|
|
24914
25023
|
return runOrDiagnose(args, { account });
|
|
24915
25024
|
});
|
|
24916
25025
|
registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
|
|
@@ -25240,7 +25349,7 @@ function registerTasksTools(server) {
|
|
|
25240
25349
|
}
|
|
25241
25350
|
|
|
25242
25351
|
// src/server.ts
|
|
25243
|
-
var VERSION = true ? "2.
|
|
25352
|
+
var VERSION = true ? "2.25.0" : "0.0.0";
|
|
25244
25353
|
var BASE_TOOL_REGISTRARS = [
|
|
25245
25354
|
registerApiTools,
|
|
25246
25355
|
registerAuthTools,
|
|
@@ -25740,17 +25849,25 @@ function useRemoteGogRunner(env = process.env) {
|
|
|
25740
25849
|
}
|
|
25741
25850
|
export {
|
|
25742
25851
|
BASE_TOOL_REGISTRARS,
|
|
25852
|
+
INLINE_ATTACHMENT_LIMITS_TEXT,
|
|
25853
|
+
MAX_INLINE_ATTACHMENT_BYTES,
|
|
25854
|
+
MAX_INLINE_ATTACHMENT_TOTAL_BYTES,
|
|
25855
|
+
MAX_REQUEST_PAYLOAD_WIRE_BYTES,
|
|
25743
25856
|
MIN_GOG_VERSION,
|
|
25744
25857
|
PAYLOAD_INLINE_MAX,
|
|
25745
25858
|
VERSION,
|
|
25746
25859
|
accountParam,
|
|
25747
25860
|
annotateTruncatedList,
|
|
25861
|
+
attachInlineParam,
|
|
25748
25862
|
authToolsFor,
|
|
25749
25863
|
diagnose,
|
|
25750
25864
|
errorText,
|
|
25751
25865
|
fetchGmailPages,
|
|
25752
25866
|
finalizeGmailSearch,
|
|
25753
25867
|
ids,
|
|
25868
|
+
inlineAttachmentArgs,
|
|
25869
|
+
inlineAttachmentSchema,
|
|
25870
|
+
inlineFileArg,
|
|
25754
25871
|
isGogFileArg,
|
|
25755
25872
|
normalizeTimestamps,
|
|
25756
25873
|
pageAliasParam,
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp",
|
|
5
5
|
"display_name": "gogcli",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.25.0",
|
|
7
7
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
"name": "gog_gmail_send",
|
|
113
|
-
"description": "Send an email"
|
|
113
|
+
"description": "Send an email, with attachments from server-side paths (attach) or from base64 bytes sent with the call (attachInline, for remote deployments with no shared filesystem)"
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
"name": "gog_gmail_run",
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.25.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.25.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|