gogcli-mcp 2.23.2 → 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 +173 -44
- package/dist/lib.js +182 -45
- package/manifest.json +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/attachments.ts +263 -0
- package/src/gmail-results.ts +15 -4
- package/src/lib.ts +14 -0
- package/src/runner.ts +170 -15
- package/src/tools/auth.ts +39 -12
- package/src/tools/calendar.ts +12 -4
- package/src/tools/gmail.ts +25 -5
- 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/auth.test.ts +60 -0
- package/tests/tools/calendar.test.ts +30 -3
- package/tests/tools/gmail.test.ts +106 -0
package/dist/lib.js
CHANGED
|
@@ -23042,7 +23042,7 @@ function activeExecutor() {
|
|
|
23042
23042
|
return runExecutor.getStore() ?? defaultExecutor;
|
|
23043
23043
|
}
|
|
23044
23044
|
var TIMEOUT_MS = 3e4;
|
|
23045
|
-
var MIN_GOG_VERSION = "0.
|
|
23045
|
+
var MIN_GOG_VERSION = "0.37.0";
|
|
23046
23046
|
function readonlyEnvEnabled() {
|
|
23047
23047
|
return readEnvVar("GOG_READONLY") !== void 0 && parseBoolEnv("GOG_READONLY", { default: true });
|
|
23048
23048
|
}
|
|
@@ -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
|
}
|
|
@@ -23712,6 +23739,7 @@ function registerApiTools(server) {
|
|
|
23712
23739
|
// src/tools/auth.ts
|
|
23713
23740
|
function registerAuthToolsWith(server, defaultServices) {
|
|
23714
23741
|
const servicesDescribe = `Services to authorize: "all" or comma-separated list (e.g. "sheets,gmail,calendar"). Default: "${defaultServices}". Prefer the narrowest set you need \u2014 requesting a service whose Google API is not enabled on the OAuth client's project makes Google reject the WHOLE request with invalid_scope.`;
|
|
23742
|
+
const extraScopesDescribe = "Additional raw OAuth scope URIs to request, comma-separated, on top of the ones `services` implies. Use for scopes no service covers \u2014 e.g. https://www.googleapis.com/auth/bigquery.readonly, required before gog_sheets_datasource_* can read BigQuery-backed Connected Sheets. Leave unset otherwise: an extra scope whose API is not enabled on the OAuth client project makes Google reject the WHOLE authorization with invalid_scope.";
|
|
23715
23743
|
server.registerTool("gog_auth_list", {
|
|
23716
23744
|
description: "List the Google accounts stored in gogcli, with their scopes. This reads local configuration only \u2014 it does not contact Google and does NOT tell you whether an account still works: a signed-out account whose refresh token expired or was revoked is listed here exactly like a healthy one, scopes and all. Use gog_auth_health to check whether an account can actually authenticate.",
|
|
23717
23745
|
annotations: { readOnlyHint: true },
|
|
@@ -23761,11 +23789,14 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
23761
23789
|
annotations: { destructiveHint: true },
|
|
23762
23790
|
inputSchema: {
|
|
23763
23791
|
email: external_exports.string().describe("Google account email to authorize"),
|
|
23764
|
-
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
|
|
23792
|
+
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
|
|
23793
|
+
extraScopes: external_exports.string().optional().describe(extraScopesDescribe)
|
|
23765
23794
|
}
|
|
23766
|
-
}, async ({ email: email3, services = defaultServices }) => {
|
|
23795
|
+
}, async ({ email: email3, services = defaultServices, extraScopes }) => {
|
|
23767
23796
|
try {
|
|
23768
|
-
|
|
23797
|
+
const args = ["auth", "add", email3, "--services", services];
|
|
23798
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, "--force-consent");
|
|
23799
|
+
return rawTextResult(await run(args, {
|
|
23769
23800
|
interactive: true,
|
|
23770
23801
|
timeout: 3e5
|
|
23771
23802
|
}));
|
|
@@ -23777,14 +23808,14 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
23777
23808
|
description: "Begin REMOTE/headless Google authorization (step 1 of 2). Returns a sign-in URL to open in any browser \u2014 no local server or terminal on the gogcli host is needed, so this works over the hosted connector where the interactive gog_auth_add cannot. Hand the URL to the user; after they sign in, the browser is redirected to a localhost URL that fails to load \u2014 that is expected. They copy that full redirected URL (from the address bar) and you pass it to gog_auth_add_complete. The link is valid for 10 minutes. If you pass a custom `services` here, pass the SAME value to gog_auth_add_complete or the second step will not match this one.",
|
|
23778
23809
|
inputSchema: {
|
|
23779
23810
|
email: external_exports.string().describe("Google account email to authorize"),
|
|
23780
|
-
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
|
|
23811
|
+
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
|
|
23812
|
+
extraScopes: external_exports.string().optional().describe(`${extraScopesDescribe} Pass the SAME value to gog_auth_add_complete.`)
|
|
23781
23813
|
}
|
|
23782
|
-
}, async ({ email: email3, services = defaultServices }) => {
|
|
23814
|
+
}, async ({ email: email3, services = defaultServices, extraScopes }) => {
|
|
23783
23815
|
try {
|
|
23784
|
-
|
|
23785
|
-
|
|
23786
|
-
|
|
23787
|
-
));
|
|
23816
|
+
const args = ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"];
|
|
23817
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
|
|
23818
|
+
return rawTextResult(await run(args, { redactMode: "tokens" }));
|
|
23788
23819
|
} catch (err) {
|
|
23789
23820
|
return errorResult(errorText(err));
|
|
23790
23821
|
}
|
|
@@ -23799,25 +23830,28 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
23799
23830
|
),
|
|
23800
23831
|
services: external_exports.string().optional().default(defaultServices).describe(
|
|
23801
23832
|
`Services authorized \u2014 MUST match the value passed to gog_auth_add_url. Default: "${defaultServices}".`
|
|
23833
|
+
),
|
|
23834
|
+
extraScopes: external_exports.string().optional().describe(
|
|
23835
|
+
"Extra OAuth scope URIs \u2014 MUST match the value passed to gog_auth_add_url, for the same reason `services` must: the two steps have to describe the same grant."
|
|
23802
23836
|
)
|
|
23803
23837
|
}
|
|
23804
|
-
}, async ({ email: email3, redirectUrl, services = defaultServices }) => {
|
|
23838
|
+
}, async ({ email: email3, redirectUrl, services = defaultServices, extraScopes }) => {
|
|
23805
23839
|
try {
|
|
23806
|
-
|
|
23807
|
-
|
|
23808
|
-
|
|
23809
|
-
|
|
23810
|
-
|
|
23811
|
-
|
|
23812
|
-
|
|
23813
|
-
|
|
23814
|
-
|
|
23815
|
-
|
|
23816
|
-
|
|
23817
|
-
|
|
23818
|
-
|
|
23819
|
-
|
|
23820
|
-
));
|
|
23840
|
+
const args = [
|
|
23841
|
+
"auth",
|
|
23842
|
+
"add",
|
|
23843
|
+
email3,
|
|
23844
|
+
"--remote",
|
|
23845
|
+
"--step",
|
|
23846
|
+
"2",
|
|
23847
|
+
"--auth-url",
|
|
23848
|
+
redirectUrl,
|
|
23849
|
+
"--services",
|
|
23850
|
+
services,
|
|
23851
|
+
"--force-consent"
|
|
23852
|
+
];
|
|
23853
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
|
|
23854
|
+
return rawTextResult(await run(args));
|
|
23821
23855
|
} catch (err) {
|
|
23822
23856
|
return errorResult(errorText(err));
|
|
23823
23857
|
}
|
|
@@ -23839,13 +23873,19 @@ function authToolsFor(defaultServices) {
|
|
|
23839
23873
|
// src/tools/calendar.ts
|
|
23840
23874
|
function registerCalendarTools(server) {
|
|
23841
23875
|
server.registerTool("gog_calendar_events", {
|
|
23842
|
-
description: 'List calendar events.
|
|
23876
|
+
description: 'List calendar events. Describe the window ONE way and one way only (gog >= 0.36.0 rejects the rest as ambiguous rather than silently discarding a flag): today on its own; or from + to; or from + days; or days on its own (a window of that many days starting today). today cannot be combined with from, to or days, and days cannot be combined with to. gog returns only 10 events by default, so a wide date range is USUALLY INCOMPLETE: raise max, or page with pageToken until the response carries no nextPageToken. A response carrying "truncated": true is an incomplete view \u2014 never conclude an event does not exist from one.',
|
|
23843
23877
|
annotations: { readOnlyHint: true },
|
|
23844
23878
|
inputSchema: {
|
|
23845
23879
|
calendarId: external_exports.string().optional().describe("Calendar ID (default: primary calendar)"),
|
|
23846
23880
|
from: external_exports.string().optional().describe("Start time filter (RFC3339, date, or natural language)"),
|
|
23847
|
-
to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language)"),
|
|
23848
|
-
|
|
23881
|
+
to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language). Mutually exclusive with today and with days."),
|
|
23882
|
+
// gog >= 0.36.0 (openclaw/gogcli#981). Before that release --days sat in
|
|
23883
|
+
// a switch arm evaluated ahead of --from, so `--from 2026-09-25 --days 5`
|
|
23884
|
+
// silently threw --from away and answered for today instead — at exit 0,
|
|
23885
|
+
// in a well-formed table. It is only exposed here now that it means what
|
|
23886
|
+
// it says.
|
|
23887
|
+
days: external_exports.number().int().positive().optional().describe('Window LENGTH in days (calendar days, DST-aware), measured from `from` when one is given and from today otherwise. Use from + days for "the week of the 25th"; days alone for "the next N days". Mutually exclusive with to and with today.'),
|
|
23888
|
+
today: external_exports.boolean().optional().describe("Only show today's events. A complete window on its own \u2014 mutually exclusive with from, to and days."),
|
|
23849
23889
|
query: external_exports.string().optional().describe("Free text search within events"),
|
|
23850
23890
|
max: external_exports.number().int().optional().describe("Max events to return. gog defaults to 10, which silently hides the rest \u2014 raise it, or page with pageToken."),
|
|
23851
23891
|
pageToken: pageTokenParam,
|
|
@@ -23855,11 +23895,12 @@ function registerCalendarTools(server) {
|
|
|
23855
23895
|
timezone: external_exports.string().optional().describe(`Display timezone for event times (IANA name, e.g. America/New_York, or "local" for the system timezone). Default: each event's timezone, then its calendar's timezone.`),
|
|
23856
23896
|
account: accountParam
|
|
23857
23897
|
}
|
|
23858
|
-
}, async ({ calendarId, from, to, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
23898
|
+
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
23859
23899
|
const args = ["calendar", "events"];
|
|
23860
23900
|
if (calendarId) args.push(calendarId);
|
|
23861
23901
|
if (from) args.push(`--from=${from}`);
|
|
23862
23902
|
if (to) args.push(`--to=${to}`);
|
|
23903
|
+
if (days !== void 0) args.push(`--days=${days}`);
|
|
23863
23904
|
if (today) args.push("--today");
|
|
23864
23905
|
if (query) args.push(`--query=${query}`);
|
|
23865
23906
|
if (max !== void 0) args.push(`--max=${max}`);
|
|
@@ -24824,6 +24865,85 @@ function finish(base, itemsKey, merged, token) {
|
|
|
24824
24865
|
return rawTextResult(JSON.stringify(out));
|
|
24825
24866
|
}
|
|
24826
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
|
+
|
|
24827
24947
|
// src/tools/gmail.ts
|
|
24828
24948
|
function registerGmailTools(server) {
|
|
24829
24949
|
server.registerTool("gog_gmail_search", {
|
|
@@ -24858,20 +24978,26 @@ function registerGmailTools(server) {
|
|
|
24858
24978
|
});
|
|
24859
24979
|
});
|
|
24860
24980
|
server.registerTool("gog_gmail_get", {
|
|
24861
|
-
description: "Get a Gmail message by ID.",
|
|
24981
|
+
description: "Get a Gmail message by ID. For a long message, sanitizeContent is the cheapest way to keep it in context: it drops the raw MIME payload and the HTML part, which are usually the bulk of the response.",
|
|
24862
24982
|
annotations: { readOnlyHint: true },
|
|
24863
24983
|
inputSchema: {
|
|
24864
24984
|
messageId: external_exports.string().describe("Message ID"),
|
|
24865
24985
|
format: external_exports.enum(["full", "metadata", "raw"]).optional().describe("Message format (default: full)"),
|
|
24986
|
+
// Requires gog >= 0.37.0. Before that (openclaw/gogcli#992) the JSON
|
|
24987
|
+
// carried the headers and body TWICE — once inside `message`, once
|
|
24988
|
+
// copied to the top level — so the flag meant to shrink the payload
|
|
24989
|
+
// enlarged it. MIN_GOG_VERSION is the guard; there is no runtime check.
|
|
24990
|
+
sanitizeContent: external_exports.boolean().optional().describe("Return agent-oriented sanitized content: HTML stripped, HTTP(S) URLs removed, raw Gmail payloads omitted from the JSON. The largest payload-size reduction available here. Note the URL removal is lossy \u2014 omit this when you need to follow a link out of the message."),
|
|
24866
24991
|
account: accountParam
|
|
24867
24992
|
}
|
|
24868
|
-
}, async ({ messageId, format, account }) => {
|
|
24993
|
+
}, async ({ messageId, format, sanitizeContent, account }) => {
|
|
24869
24994
|
const args = ["gmail", "get", messageId];
|
|
24870
24995
|
if (format) args.push(`--format=${format}`);
|
|
24996
|
+
if (sanitizeContent) args.push("--sanitize-content");
|
|
24871
24997
|
return runOrDiagnose(args, { account });
|
|
24872
24998
|
});
|
|
24873
24999
|
server.registerTool("gog_gmail_send", {
|
|
24874
|
-
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.',
|
|
24875
25001
|
annotations: { destructiveHint: true },
|
|
24876
25002
|
inputSchema: {
|
|
24877
25003
|
to: external_exports.string().describe("Recipient(s), comma-separated"),
|
|
@@ -24881,16 +25007,19 @@ function registerGmailTools(server) {
|
|
|
24881
25007
|
bcc: external_exports.string().optional().describe("BCC recipients, comma-separated"),
|
|
24882
25008
|
replyToMessageId: external_exports.string().optional().describe("Message ID to reply to"),
|
|
24883
25009
|
threadId: external_exports.string().optional().describe("Thread ID to reply within"),
|
|
24884
|
-
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,
|
|
24885
25012
|
account: accountParam
|
|
24886
25013
|
}
|
|
24887
|
-
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, account }) => {
|
|
25014
|
+
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, attachInline, account }) => {
|
|
24888
25015
|
const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
|
|
24889
25016
|
if (cc) args.push(`--cc=${cc}`);
|
|
24890
25017
|
if (bcc) args.push(`--bcc=${bcc}`);
|
|
24891
25018
|
if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
|
|
24892
25019
|
if (threadId) args.push(`--thread-id=${threadId}`);
|
|
24893
25020
|
if (attach) for (const path of attach) args.push(`--attach=${path}`);
|
|
25021
|
+
const inline = inlineAttachmentArgs("attach", attachInline, args);
|
|
25022
|
+
args.push(...inline);
|
|
24894
25023
|
return runOrDiagnose(args, { account });
|
|
24895
25024
|
});
|
|
24896
25025
|
registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
|
|
@@ -25220,7 +25349,7 @@ function registerTasksTools(server) {
|
|
|
25220
25349
|
}
|
|
25221
25350
|
|
|
25222
25351
|
// src/server.ts
|
|
25223
|
-
var VERSION = true ? "2.
|
|
25352
|
+
var VERSION = true ? "2.25.0" : "0.0.0";
|
|
25224
25353
|
var BASE_TOOL_REGISTRARS = [
|
|
25225
25354
|
registerApiTools,
|
|
25226
25355
|
registerAuthTools,
|
|
@@ -25720,17 +25849,25 @@ function useRemoteGogRunner(env = process.env) {
|
|
|
25720
25849
|
}
|
|
25721
25850
|
export {
|
|
25722
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,
|
|
25723
25856
|
MIN_GOG_VERSION,
|
|
25724
25857
|
PAYLOAD_INLINE_MAX,
|
|
25725
25858
|
VERSION,
|
|
25726
25859
|
accountParam,
|
|
25727
25860
|
annotateTruncatedList,
|
|
25861
|
+
attachInlineParam,
|
|
25728
25862
|
authToolsFor,
|
|
25729
25863
|
diagnose,
|
|
25730
25864
|
errorText,
|
|
25731
25865
|
fetchGmailPages,
|
|
25732
25866
|
finalizeGmailSearch,
|
|
25733
25867
|
ids,
|
|
25868
|
+
inlineAttachmentArgs,
|
|
25869
|
+
inlineAttachmentSchema,
|
|
25870
|
+
inlineFileArg,
|
|
25734
25871
|
isGogFileArg,
|
|
25735
25872
|
normalizeTimestamps,
|
|
25736
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
|
},
|