gogcli-mcp 2.28.0 → 2.29.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 +147 -15
- package/dist/lib.js +147 -15
- package/manifest.json +5 -5
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/tools/calendar.ts +32 -2
- package/src/tools/drive.ts +48 -7
- package/src/tools/utils.ts +85 -4
- package/src/worker.ts +1 -1
- package/tests/tools/calendar.test.ts +44 -20
- package/tests/tools/drive.test.ts +81 -13
- package/tests/tools/utils.test.ts +141 -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.29.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.29.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/index.js
CHANGED
|
@@ -31323,6 +31323,77 @@ function redactSecrets(text) {
|
|
|
31323
31323
|
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
31324
31324
|
}
|
|
31325
31325
|
|
|
31326
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/response/view.js
|
|
31327
|
+
var VIEWS = ["compact", "full", "raw"];
|
|
31328
|
+
var DEFAULT_VIEW = "compact";
|
|
31329
|
+
var BLURB = {
|
|
31330
|
+
compact: '"compact" (default) drops fields the response already carries elsewhere',
|
|
31331
|
+
full: '"full" returns every field this server understands',
|
|
31332
|
+
raw: '"raw" returns the upstream payload unprojected'
|
|
31333
|
+
};
|
|
31334
|
+
function viewParam(honoured, opts = {}) {
|
|
31335
|
+
if (honoured.length < 2) {
|
|
31336
|
+
throw new Error("viewParam needs at least two rungs: a parameter offering one value decides nothing");
|
|
31337
|
+
}
|
|
31338
|
+
if (!honoured.includes("compact")) {
|
|
31339
|
+
throw new Error('viewParam must offer "compact": a tool with no cheap rung has nothing to default to');
|
|
31340
|
+
}
|
|
31341
|
+
const ordered = VIEWS.filter((v) => honoured.includes(v));
|
|
31342
|
+
const sentence = `Response shape: ${ordered.map((v) => BLURB[v]).join("; ")}.`;
|
|
31343
|
+
return external_exports.enum(Object.fromEntries(ordered.map((v) => [v, v]))).optional().describe(opts.note ? `${sentence} ${opts.note}` : sentence);
|
|
31344
|
+
}
|
|
31345
|
+
function resolveView(value, honoured) {
|
|
31346
|
+
return value !== void 0 && honoured.includes(value) ? value : DEFAULT_VIEW;
|
|
31347
|
+
}
|
|
31348
|
+
function minifiedResult(data) {
|
|
31349
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
31350
|
+
}
|
|
31351
|
+
|
|
31352
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/response/media.js
|
|
31353
|
+
var MEDIA_NOUN = "(?:avatar|tall_avatar|cover_photo|cover_image|picture|photo|thumbnail|thumb|image|icon|banner|profile_pic(?:ture)?|logo)";
|
|
31354
|
+
var MEDIA_KEY = new RegExp(`^${MEDIA_NOUN}s?(?:(?:link|uri|url)s?)?$`, "i");
|
|
31355
|
+
var MEDIA_URL = /^https?:\/\/[^\s]+?\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)([?#]|$)/i;
|
|
31356
|
+
function stripMediaUrls(value, opts = {}) {
|
|
31357
|
+
const keep = new Set((opts.keep ?? []).map((k) => k.toLowerCase()));
|
|
31358
|
+
const drop = (opts.drop ?? []).map((rule) => typeof rule === "string" ? rule.toLowerCase() : new RegExp(rule.source, rule.flags));
|
|
31359
|
+
return walk(value, keep, drop);
|
|
31360
|
+
}
|
|
31361
|
+
function alsoDrop(key, drop) {
|
|
31362
|
+
const lower = key.toLowerCase();
|
|
31363
|
+
for (const rule of drop) {
|
|
31364
|
+
if (typeof rule === "string") {
|
|
31365
|
+
if (rule === lower)
|
|
31366
|
+
return true;
|
|
31367
|
+
continue;
|
|
31368
|
+
}
|
|
31369
|
+
rule.lastIndex = 0;
|
|
31370
|
+
if (rule.test(key))
|
|
31371
|
+
return true;
|
|
31372
|
+
}
|
|
31373
|
+
return false;
|
|
31374
|
+
}
|
|
31375
|
+
function walk(value, keep, drop) {
|
|
31376
|
+
if (Array.isArray(value))
|
|
31377
|
+
return value.map((v) => walk(v, keep, drop));
|
|
31378
|
+
if (value === null || typeof value !== "object")
|
|
31379
|
+
return value;
|
|
31380
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
31381
|
+
return value;
|
|
31382
|
+
const out = {};
|
|
31383
|
+
for (const [key, v] of Object.entries(value)) {
|
|
31384
|
+
if (keep.has(key.toLowerCase())) {
|
|
31385
|
+
out[key] = v;
|
|
31386
|
+
continue;
|
|
31387
|
+
}
|
|
31388
|
+
if (MEDIA_KEY.test(key) || alsoDrop(key, drop))
|
|
31389
|
+
continue;
|
|
31390
|
+
if (typeof v === "string" && MEDIA_URL.test(v))
|
|
31391
|
+
continue;
|
|
31392
|
+
out[key] = walk(v, keep, drop);
|
|
31393
|
+
}
|
|
31394
|
+
return out;
|
|
31395
|
+
}
|
|
31396
|
+
|
|
31326
31397
|
// ../../node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
31327
31398
|
function rawTextResult(text) {
|
|
31328
31399
|
return { content: [{ type: "text", text }] };
|
|
@@ -31852,11 +31923,11 @@ function parseTimestampValue(key, value, assumeNaiveIn) {
|
|
|
31852
31923
|
}
|
|
31853
31924
|
return null;
|
|
31854
31925
|
}
|
|
31855
|
-
function
|
|
31926
|
+
function walk2(node, tz, naiveTz) {
|
|
31856
31927
|
let changed = false;
|
|
31857
31928
|
if (Array.isArray(node)) {
|
|
31858
31929
|
for (const item of node) {
|
|
31859
|
-
if (
|
|
31930
|
+
if (walk2(item, tz, naiveTz)) changed = true;
|
|
31860
31931
|
}
|
|
31861
31932
|
return changed;
|
|
31862
31933
|
}
|
|
@@ -31865,7 +31936,7 @@ function walk(node, tz, naiveTz) {
|
|
|
31865
31936
|
for (const key of Object.keys(obj)) {
|
|
31866
31937
|
const value = obj[key];
|
|
31867
31938
|
if (value !== null && typeof value === "object") {
|
|
31868
|
-
if (
|
|
31939
|
+
if (walk2(value, tz, naiveTz)) changed = true;
|
|
31869
31940
|
continue;
|
|
31870
31941
|
}
|
|
31871
31942
|
if (ZONE_NAME_KEYS.has(key) || !TIMESTAMP_KEYS.has(key)) continue;
|
|
@@ -31891,7 +31962,7 @@ function normalizeTimestamps(text, tz = displayTimeZone(), naiveTz = naiveSource
|
|
|
31891
31962
|
} catch {
|
|
31892
31963
|
return text;
|
|
31893
31964
|
}
|
|
31894
|
-
if (!
|
|
31965
|
+
if (!walk2(parsed, tz, naiveTz)) return text;
|
|
31895
31966
|
return JSON.stringify(parsed, null, detectIndent(text));
|
|
31896
31967
|
}
|
|
31897
31968
|
|
|
@@ -32069,10 +32140,38 @@ ${accounts || "(none)"}${hint}`);
|
|
|
32069
32140
|
return errorResult(`${errText}${hint}`);
|
|
32070
32141
|
}
|
|
32071
32142
|
}
|
|
32143
|
+
function minifiedOrRawText(text, stripMedia = false) {
|
|
32144
|
+
const trimmed = text.trim();
|
|
32145
|
+
if (trimmed === "" || !/^[[{]/.test(trimmed)) return rawTextResult(text);
|
|
32146
|
+
try {
|
|
32147
|
+
const parsed = JSON.parse(trimmed);
|
|
32148
|
+
return minifiedResult(stripMedia ? stripMediaUrls(parsed) : parsed);
|
|
32149
|
+
} catch {
|
|
32150
|
+
return rawTextResult(text);
|
|
32151
|
+
}
|
|
32152
|
+
}
|
|
32153
|
+
function isRejectedFieldMask(err) {
|
|
32154
|
+
return /invalidParameter|invalid field selection/i.test(String(err));
|
|
32155
|
+
}
|
|
32156
|
+
async function runProjected(args, options) {
|
|
32157
|
+
const { fieldsMask } = options;
|
|
32158
|
+
if (!fieldsMask) return run(args, options);
|
|
32159
|
+
try {
|
|
32160
|
+
return await run([...args, `--fields=${fieldsMask}`], options);
|
|
32161
|
+
} catch (err) {
|
|
32162
|
+
if (!isRejectedFieldMask(err)) throw err;
|
|
32163
|
+
process.stderr.write(
|
|
32164
|
+
`gogcli-mcp: Google rejected the compact field mask (${fieldsMask}); retrying unprojected
|
|
32165
|
+
`
|
|
32166
|
+
);
|
|
32167
|
+
return run(args, options);
|
|
32168
|
+
}
|
|
32169
|
+
}
|
|
32072
32170
|
async function runOrDiagnose(args, options) {
|
|
32073
32171
|
try {
|
|
32074
|
-
const raw = await
|
|
32075
|
-
|
|
32172
|
+
const raw = await runProjected(args, options);
|
|
32173
|
+
if (options.lossless) return rawTextResult(raw);
|
|
32174
|
+
return minifiedOrRawText(stripConsumedPageToken(normalizeTimestamps(raw)), options.stripMedia);
|
|
32076
32175
|
} catch (err) {
|
|
32077
32176
|
return diagnose(err);
|
|
32078
32177
|
}
|
|
@@ -32446,6 +32545,7 @@ function pushReminderFlags(args, p) {
|
|
|
32446
32545
|
}
|
|
32447
32546
|
for (const reminder of p.reminders) args.push(`--reminder=${reminder}`);
|
|
32448
32547
|
}
|
|
32548
|
+
var CALENDAR_EVENTS_COMPACT_FIELDS = "nextPageToken,items(id,summary,start,end,location,status,htmlLink)";
|
|
32449
32549
|
function registerCalendarTools(server) {
|
|
32450
32550
|
server.registerTool("gog_calendar_events", {
|
|
32451
32551
|
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.',
|
|
@@ -32468,9 +32568,12 @@ function registerCalendarTools(server) {
|
|
|
32468
32568
|
all: external_exports.boolean().optional().describe('Fetch events from ALL CALENDARS. NOTE: unlike the gmail search tools, this does NOT mean "all pages" \u2014 it widens the calendar set, not the page window. Use pageToken to reach later pages.'),
|
|
32469
32569
|
eventTypes: external_exports.array(external_exports.enum(["default", "birthday", "focus-time", "from-gmail", "out-of-office", "working-location"])).optional().describe("Filter to specific event types (repeatable)"),
|
|
32470
32570
|
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.`),
|
|
32571
|
+
view: viewParam(["compact", "full"], {
|
|
32572
|
+
note: "compact (the default) drops description and attendees \u2014 together two thirds of a listing's bytes \u2014 plus etag/iCalUID/kind. Ask for full when you need a body or a guest list."
|
|
32573
|
+
}),
|
|
32471
32574
|
account: accountParam
|
|
32472
32575
|
}
|
|
32473
|
-
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
32576
|
+
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, view, account }) => {
|
|
32474
32577
|
const args = ["calendar", "events"];
|
|
32475
32578
|
if (calendarId) args.push(calendarId);
|
|
32476
32579
|
if (from) args.push(`--from=${from}`);
|
|
@@ -32484,7 +32587,11 @@ function registerCalendarTools(server) {
|
|
|
32484
32587
|
if (all) args.push("--all");
|
|
32485
32588
|
if (eventTypes) for (const t of eventTypes) args.push(`--event-types=${t}`);
|
|
32486
32589
|
if (timezone) args.push(`--timezone=${timezone}`);
|
|
32487
|
-
const
|
|
32590
|
+
const rung = resolveView(view, ["compact", "full"]);
|
|
32591
|
+
const result = await runOrDiagnose(args, {
|
|
32592
|
+
account,
|
|
32593
|
+
fieldsMask: rung === "compact" ? CALENDAR_EVENTS_COMPACT_FIELDS : void 0
|
|
32594
|
+
});
|
|
32488
32595
|
return annotateTruncatedList(result, "events");
|
|
32489
32596
|
});
|
|
32490
32597
|
server.registerTool("gog_calendar_get", {
|
|
@@ -33417,6 +33524,7 @@ function fileMeta(raw) {
|
|
|
33417
33524
|
const f = parsed.file ?? parsed;
|
|
33418
33525
|
return { name: f.name, mimeType: f.mimeType };
|
|
33419
33526
|
}
|
|
33527
|
+
var DRIVE_LS_COMPACT_FIELDS = "nextPageToken,files(id,name,mimeType,modifiedTime,size,webViewLink)";
|
|
33420
33528
|
function registerDriveTools(server) {
|
|
33421
33529
|
server.registerTool("gog_drive_ls", {
|
|
33422
33530
|
description: "List files in a Google Drive folder (default: root).",
|
|
@@ -33428,9 +33536,12 @@ function registerDriveTools(server) {
|
|
|
33428
33536
|
page: pageAliasParam,
|
|
33429
33537
|
query: external_exports.string().optional().describe(`Drive query filter (e.g. "name contains 'budget'")`),
|
|
33430
33538
|
allDrives: external_exports.boolean().optional().describe("Include shared drives (default: true). Set false for My Drive only."),
|
|
33539
|
+
view: viewParam(["compact", "full"], {
|
|
33540
|
+
note: "compact (the default) drops owners, parents, thumbnailLink and hasThumbnail \u2014 near-constant across a listing and 48% of its bytes. Ask for full to get them."
|
|
33541
|
+
}),
|
|
33431
33542
|
account: accountParam
|
|
33432
33543
|
}
|
|
33433
|
-
}, async ({ folderId, max, pageToken, page, query, allDrives, account }) => {
|
|
33544
|
+
}, async ({ folderId, max, pageToken, page, query, allDrives, view, account }) => {
|
|
33434
33545
|
const args = ["drive", "ls"];
|
|
33435
33546
|
if (folderId) args.push(`--parent=${folderId}`);
|
|
33436
33547
|
if (max !== void 0) args.push(`--max=${max}`);
|
|
@@ -33438,27 +33549,48 @@ function registerDriveTools(server) {
|
|
|
33438
33549
|
if (token) args.push(`--page=${token}`);
|
|
33439
33550
|
if (query) args.push(`--query=${query}`);
|
|
33440
33551
|
if (allDrives === false) args.push("--no-all-drives");
|
|
33441
|
-
|
|
33552
|
+
const rung = resolveView(view, ["compact", "full"]);
|
|
33553
|
+
return runOrDiagnose(args, {
|
|
33554
|
+
account,
|
|
33555
|
+
fieldsMask: rung === "compact" ? DRIVE_LS_COMPACT_FIELDS : void 0
|
|
33556
|
+
});
|
|
33442
33557
|
});
|
|
33443
33558
|
server.registerTool("gog_drive_search", {
|
|
33444
33559
|
description: "Search Google Drive files by full-text query.",
|
|
33445
33560
|
annotations: { readOnlyHint: true },
|
|
33446
33561
|
inputSchema: {
|
|
33447
33562
|
query: external_exports.string().describe("Search query"),
|
|
33563
|
+
// gog's `drive search` accepts no --fields mask, so unlike gog_drive_ls
|
|
33564
|
+
// this tool's compact rung is a LOCAL projection. Same vocabulary either
|
|
33565
|
+
// way: a caller does not need to know which lever is being pulled.
|
|
33566
|
+
view: viewParam(["compact", "full"], { note: "compact (the default) drops thumbnailLink \u2014 a URL a model cannot see, and 30%+ of a Drive file record. Ask for full to get it back." }),
|
|
33448
33567
|
account: accountParam
|
|
33449
33568
|
}
|
|
33450
|
-
}, async ({ query, account }) => {
|
|
33451
|
-
return runOrDiagnose(["drive", "search", query], {
|
|
33569
|
+
}, async ({ query, view, account }) => {
|
|
33570
|
+
return runOrDiagnose(["drive", "search", query], {
|
|
33571
|
+
account,
|
|
33572
|
+
stripMedia: resolveView(view, ["compact", "full"]) === "compact"
|
|
33573
|
+
});
|
|
33452
33574
|
});
|
|
33453
33575
|
server.registerTool("gog_drive_get", {
|
|
33454
33576
|
description: "Get metadata for a Google Drive file.",
|
|
33455
33577
|
annotations: { readOnlyHint: true },
|
|
33456
33578
|
inputSchema: {
|
|
33457
33579
|
fileId: external_exports.string().describe("File ID"),
|
|
33580
|
+
// A --fields mask saves only 7% here: the default set is already narrow,
|
|
33581
|
+
// which is why this tool takes no mask. The media strip saves 27.5% of
|
|
33582
|
+
// the tool's actual output, measured end to end over stdio, which is why
|
|
33583
|
+
// it takes a view after all. (An earlier note said 32.9%; that was a
|
|
33584
|
+
// minified-vs-stripped comparison of the raw gog payload rather than of
|
|
33585
|
+
// what the tool returns. The end-to-end figure is the one a caller sees.)
|
|
33586
|
+
view: viewParam(["compact", "full"], { note: "compact (the default) drops thumbnailLink \u2014 a URL a model cannot see, and 30%+ of a Drive file record. Ask for full to get it back." }),
|
|
33458
33587
|
account: accountParam
|
|
33459
33588
|
}
|
|
33460
|
-
}, async ({ fileId, account }) => {
|
|
33461
|
-
return runOrDiagnose(["drive", "get", fileId], {
|
|
33589
|
+
}, async ({ fileId, view, account }) => {
|
|
33590
|
+
return runOrDiagnose(["drive", "get", fileId], {
|
|
33591
|
+
account,
|
|
33592
|
+
stripMedia: resolveView(view, ["compact", "full"]) === "compact"
|
|
33593
|
+
});
|
|
33462
33594
|
});
|
|
33463
33595
|
server.registerTool("gog_drive_mkdir", {
|
|
33464
33596
|
description: "Create a new folder in Google Drive.",
|
|
@@ -34191,7 +34323,7 @@ function registerTasksTools(server) {
|
|
|
34191
34323
|
}
|
|
34192
34324
|
|
|
34193
34325
|
// src/server.ts
|
|
34194
|
-
var VERSION = true ? "2.
|
|
34326
|
+
var VERSION = true ? "2.29.0" : "0.0.0";
|
|
34195
34327
|
var BASE_TOOL_REGISTRARS = [
|
|
34196
34328
|
registerApiTools,
|
|
34197
34329
|
registerAppScriptTools,
|
package/dist/lib.js
CHANGED
|
@@ -23237,6 +23237,77 @@ function redactSecrets(text) {
|
|
|
23237
23237
|
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
23238
23238
|
}
|
|
23239
23239
|
|
|
23240
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/response/view.js
|
|
23241
|
+
var VIEWS = ["compact", "full", "raw"];
|
|
23242
|
+
var DEFAULT_VIEW = "compact";
|
|
23243
|
+
var BLURB = {
|
|
23244
|
+
compact: '"compact" (default) drops fields the response already carries elsewhere',
|
|
23245
|
+
full: '"full" returns every field this server understands',
|
|
23246
|
+
raw: '"raw" returns the upstream payload unprojected'
|
|
23247
|
+
};
|
|
23248
|
+
function viewParam(honoured, opts = {}) {
|
|
23249
|
+
if (honoured.length < 2) {
|
|
23250
|
+
throw new Error("viewParam needs at least two rungs: a parameter offering one value decides nothing");
|
|
23251
|
+
}
|
|
23252
|
+
if (!honoured.includes("compact")) {
|
|
23253
|
+
throw new Error('viewParam must offer "compact": a tool with no cheap rung has nothing to default to');
|
|
23254
|
+
}
|
|
23255
|
+
const ordered = VIEWS.filter((v) => honoured.includes(v));
|
|
23256
|
+
const sentence = `Response shape: ${ordered.map((v) => BLURB[v]).join("; ")}.`;
|
|
23257
|
+
return external_exports.enum(Object.fromEntries(ordered.map((v) => [v, v]))).optional().describe(opts.note ? `${sentence} ${opts.note}` : sentence);
|
|
23258
|
+
}
|
|
23259
|
+
function resolveView(value, honoured) {
|
|
23260
|
+
return value !== void 0 && honoured.includes(value) ? value : DEFAULT_VIEW;
|
|
23261
|
+
}
|
|
23262
|
+
function minifiedResult(data) {
|
|
23263
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
23264
|
+
}
|
|
23265
|
+
|
|
23266
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/response/media.js
|
|
23267
|
+
var MEDIA_NOUN = "(?:avatar|tall_avatar|cover_photo|cover_image|picture|photo|thumbnail|thumb|image|icon|banner|profile_pic(?:ture)?|logo)";
|
|
23268
|
+
var MEDIA_KEY = new RegExp(`^${MEDIA_NOUN}s?(?:(?:link|uri|url)s?)?$`, "i");
|
|
23269
|
+
var MEDIA_URL = /^https?:\/\/[^\s]+?\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)([?#]|$)/i;
|
|
23270
|
+
function stripMediaUrls(value, opts = {}) {
|
|
23271
|
+
const keep = new Set((opts.keep ?? []).map((k) => k.toLowerCase()));
|
|
23272
|
+
const drop = (opts.drop ?? []).map((rule) => typeof rule === "string" ? rule.toLowerCase() : new RegExp(rule.source, rule.flags));
|
|
23273
|
+
return walk(value, keep, drop);
|
|
23274
|
+
}
|
|
23275
|
+
function alsoDrop(key, drop) {
|
|
23276
|
+
const lower = key.toLowerCase();
|
|
23277
|
+
for (const rule of drop) {
|
|
23278
|
+
if (typeof rule === "string") {
|
|
23279
|
+
if (rule === lower)
|
|
23280
|
+
return true;
|
|
23281
|
+
continue;
|
|
23282
|
+
}
|
|
23283
|
+
rule.lastIndex = 0;
|
|
23284
|
+
if (rule.test(key))
|
|
23285
|
+
return true;
|
|
23286
|
+
}
|
|
23287
|
+
return false;
|
|
23288
|
+
}
|
|
23289
|
+
function walk(value, keep, drop) {
|
|
23290
|
+
if (Array.isArray(value))
|
|
23291
|
+
return value.map((v) => walk(v, keep, drop));
|
|
23292
|
+
if (value === null || typeof value !== "object")
|
|
23293
|
+
return value;
|
|
23294
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
23295
|
+
return value;
|
|
23296
|
+
const out = {};
|
|
23297
|
+
for (const [key, v] of Object.entries(value)) {
|
|
23298
|
+
if (keep.has(key.toLowerCase())) {
|
|
23299
|
+
out[key] = v;
|
|
23300
|
+
continue;
|
|
23301
|
+
}
|
|
23302
|
+
if (MEDIA_KEY.test(key) || alsoDrop(key, drop))
|
|
23303
|
+
continue;
|
|
23304
|
+
if (typeof v === "string" && MEDIA_URL.test(v))
|
|
23305
|
+
continue;
|
|
23306
|
+
out[key] = walk(v, keep, drop);
|
|
23307
|
+
}
|
|
23308
|
+
return out;
|
|
23309
|
+
}
|
|
23310
|
+
|
|
23240
23311
|
// ../../node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
23241
23312
|
function rawTextResult(text) {
|
|
23242
23313
|
return { content: [{ type: "text", text }] };
|
|
@@ -23698,11 +23769,11 @@ function parseTimestampValue(key, value, assumeNaiveIn) {
|
|
|
23698
23769
|
}
|
|
23699
23770
|
return null;
|
|
23700
23771
|
}
|
|
23701
|
-
function
|
|
23772
|
+
function walk2(node, tz, naiveTz) {
|
|
23702
23773
|
let changed = false;
|
|
23703
23774
|
if (Array.isArray(node)) {
|
|
23704
23775
|
for (const item of node) {
|
|
23705
|
-
if (
|
|
23776
|
+
if (walk2(item, tz, naiveTz)) changed = true;
|
|
23706
23777
|
}
|
|
23707
23778
|
return changed;
|
|
23708
23779
|
}
|
|
@@ -23711,7 +23782,7 @@ function walk(node, tz, naiveTz) {
|
|
|
23711
23782
|
for (const key of Object.keys(obj)) {
|
|
23712
23783
|
const value = obj[key];
|
|
23713
23784
|
if (value !== null && typeof value === "object") {
|
|
23714
|
-
if (
|
|
23785
|
+
if (walk2(value, tz, naiveTz)) changed = true;
|
|
23715
23786
|
continue;
|
|
23716
23787
|
}
|
|
23717
23788
|
if (ZONE_NAME_KEYS.has(key) || !TIMESTAMP_KEYS.has(key)) continue;
|
|
@@ -23737,7 +23808,7 @@ function normalizeTimestamps(text, tz = displayTimeZone(), naiveTz = naiveSource
|
|
|
23737
23808
|
} catch {
|
|
23738
23809
|
return text;
|
|
23739
23810
|
}
|
|
23740
|
-
if (!
|
|
23811
|
+
if (!walk2(parsed, tz, naiveTz)) return text;
|
|
23741
23812
|
return JSON.stringify(parsed, null, detectIndent(text));
|
|
23742
23813
|
}
|
|
23743
23814
|
|
|
@@ -23915,10 +23986,38 @@ ${accounts || "(none)"}${hint}`);
|
|
|
23915
23986
|
return errorResult(`${errText}${hint}`);
|
|
23916
23987
|
}
|
|
23917
23988
|
}
|
|
23989
|
+
function minifiedOrRawText(text, stripMedia = false) {
|
|
23990
|
+
const trimmed = text.trim();
|
|
23991
|
+
if (trimmed === "" || !/^[[{]/.test(trimmed)) return rawTextResult(text);
|
|
23992
|
+
try {
|
|
23993
|
+
const parsed = JSON.parse(trimmed);
|
|
23994
|
+
return minifiedResult(stripMedia ? stripMediaUrls(parsed) : parsed);
|
|
23995
|
+
} catch {
|
|
23996
|
+
return rawTextResult(text);
|
|
23997
|
+
}
|
|
23998
|
+
}
|
|
23999
|
+
function isRejectedFieldMask(err) {
|
|
24000
|
+
return /invalidParameter|invalid field selection/i.test(String(err));
|
|
24001
|
+
}
|
|
24002
|
+
async function runProjected(args, options) {
|
|
24003
|
+
const { fieldsMask } = options;
|
|
24004
|
+
if (!fieldsMask) return run(args, options);
|
|
24005
|
+
try {
|
|
24006
|
+
return await run([...args, `--fields=${fieldsMask}`], options);
|
|
24007
|
+
} catch (err) {
|
|
24008
|
+
if (!isRejectedFieldMask(err)) throw err;
|
|
24009
|
+
process.stderr.write(
|
|
24010
|
+
`gogcli-mcp: Google rejected the compact field mask (${fieldsMask}); retrying unprojected
|
|
24011
|
+
`
|
|
24012
|
+
);
|
|
24013
|
+
return run(args, options);
|
|
24014
|
+
}
|
|
24015
|
+
}
|
|
23918
24016
|
async function runOrDiagnose(args, options) {
|
|
23919
24017
|
try {
|
|
23920
|
-
const raw = await
|
|
23921
|
-
|
|
24018
|
+
const raw = await runProjected(args, options);
|
|
24019
|
+
if (options.lossless) return rawTextResult(raw);
|
|
24020
|
+
return minifiedOrRawText(stripConsumedPageToken(normalizeTimestamps(raw)), options.stripMedia);
|
|
23922
24021
|
} catch (err) {
|
|
23923
24022
|
return diagnose(err);
|
|
23924
24023
|
}
|
|
@@ -24295,6 +24394,7 @@ function pushReminderFlags(args, p) {
|
|
|
24295
24394
|
}
|
|
24296
24395
|
for (const reminder of p.reminders) args.push(`--reminder=${reminder}`);
|
|
24297
24396
|
}
|
|
24397
|
+
var CALENDAR_EVENTS_COMPACT_FIELDS = "nextPageToken,items(id,summary,start,end,location,status,htmlLink)";
|
|
24298
24398
|
function registerCalendarTools(server) {
|
|
24299
24399
|
server.registerTool("gog_calendar_events", {
|
|
24300
24400
|
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.',
|
|
@@ -24317,9 +24417,12 @@ function registerCalendarTools(server) {
|
|
|
24317
24417
|
all: external_exports.boolean().optional().describe('Fetch events from ALL CALENDARS. NOTE: unlike the gmail search tools, this does NOT mean "all pages" \u2014 it widens the calendar set, not the page window. Use pageToken to reach later pages.'),
|
|
24318
24418
|
eventTypes: external_exports.array(external_exports.enum(["default", "birthday", "focus-time", "from-gmail", "out-of-office", "working-location"])).optional().describe("Filter to specific event types (repeatable)"),
|
|
24319
24419
|
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.`),
|
|
24420
|
+
view: viewParam(["compact", "full"], {
|
|
24421
|
+
note: "compact (the default) drops description and attendees \u2014 together two thirds of a listing's bytes \u2014 plus etag/iCalUID/kind. Ask for full when you need a body or a guest list."
|
|
24422
|
+
}),
|
|
24320
24423
|
account: accountParam
|
|
24321
24424
|
}
|
|
24322
|
-
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
24425
|
+
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, view, account }) => {
|
|
24323
24426
|
const args = ["calendar", "events"];
|
|
24324
24427
|
if (calendarId) args.push(calendarId);
|
|
24325
24428
|
if (from) args.push(`--from=${from}`);
|
|
@@ -24333,7 +24436,11 @@ function registerCalendarTools(server) {
|
|
|
24333
24436
|
if (all) args.push("--all");
|
|
24334
24437
|
if (eventTypes) for (const t of eventTypes) args.push(`--event-types=${t}`);
|
|
24335
24438
|
if (timezone) args.push(`--timezone=${timezone}`);
|
|
24336
|
-
const
|
|
24439
|
+
const rung = resolveView(view, ["compact", "full"]);
|
|
24440
|
+
const result = await runOrDiagnose(args, {
|
|
24441
|
+
account,
|
|
24442
|
+
fieldsMask: rung === "compact" ? CALENDAR_EVENTS_COMPACT_FIELDS : void 0
|
|
24443
|
+
});
|
|
24337
24444
|
return annotateTruncatedList(result, "events");
|
|
24338
24445
|
});
|
|
24339
24446
|
server.registerTool("gog_calendar_get", {
|
|
@@ -25266,6 +25373,7 @@ function fileMeta(raw) {
|
|
|
25266
25373
|
const f = parsed.file ?? parsed;
|
|
25267
25374
|
return { name: f.name, mimeType: f.mimeType };
|
|
25268
25375
|
}
|
|
25376
|
+
var DRIVE_LS_COMPACT_FIELDS = "nextPageToken,files(id,name,mimeType,modifiedTime,size,webViewLink)";
|
|
25269
25377
|
function registerDriveTools(server) {
|
|
25270
25378
|
server.registerTool("gog_drive_ls", {
|
|
25271
25379
|
description: "List files in a Google Drive folder (default: root).",
|
|
@@ -25277,9 +25385,12 @@ function registerDriveTools(server) {
|
|
|
25277
25385
|
page: pageAliasParam,
|
|
25278
25386
|
query: external_exports.string().optional().describe(`Drive query filter (e.g. "name contains 'budget'")`),
|
|
25279
25387
|
allDrives: external_exports.boolean().optional().describe("Include shared drives (default: true). Set false for My Drive only."),
|
|
25388
|
+
view: viewParam(["compact", "full"], {
|
|
25389
|
+
note: "compact (the default) drops owners, parents, thumbnailLink and hasThumbnail \u2014 near-constant across a listing and 48% of its bytes. Ask for full to get them."
|
|
25390
|
+
}),
|
|
25280
25391
|
account: accountParam
|
|
25281
25392
|
}
|
|
25282
|
-
}, async ({ folderId, max, pageToken, page, query, allDrives, account }) => {
|
|
25393
|
+
}, async ({ folderId, max, pageToken, page, query, allDrives, view, account }) => {
|
|
25283
25394
|
const args = ["drive", "ls"];
|
|
25284
25395
|
if (folderId) args.push(`--parent=${folderId}`);
|
|
25285
25396
|
if (max !== void 0) args.push(`--max=${max}`);
|
|
@@ -25287,27 +25398,48 @@ function registerDriveTools(server) {
|
|
|
25287
25398
|
if (token) args.push(`--page=${token}`);
|
|
25288
25399
|
if (query) args.push(`--query=${query}`);
|
|
25289
25400
|
if (allDrives === false) args.push("--no-all-drives");
|
|
25290
|
-
|
|
25401
|
+
const rung = resolveView(view, ["compact", "full"]);
|
|
25402
|
+
return runOrDiagnose(args, {
|
|
25403
|
+
account,
|
|
25404
|
+
fieldsMask: rung === "compact" ? DRIVE_LS_COMPACT_FIELDS : void 0
|
|
25405
|
+
});
|
|
25291
25406
|
});
|
|
25292
25407
|
server.registerTool("gog_drive_search", {
|
|
25293
25408
|
description: "Search Google Drive files by full-text query.",
|
|
25294
25409
|
annotations: { readOnlyHint: true },
|
|
25295
25410
|
inputSchema: {
|
|
25296
25411
|
query: external_exports.string().describe("Search query"),
|
|
25412
|
+
// gog's `drive search` accepts no --fields mask, so unlike gog_drive_ls
|
|
25413
|
+
// this tool's compact rung is a LOCAL projection. Same vocabulary either
|
|
25414
|
+
// way: a caller does not need to know which lever is being pulled.
|
|
25415
|
+
view: viewParam(["compact", "full"], { note: "compact (the default) drops thumbnailLink \u2014 a URL a model cannot see, and 30%+ of a Drive file record. Ask for full to get it back." }),
|
|
25297
25416
|
account: accountParam
|
|
25298
25417
|
}
|
|
25299
|
-
}, async ({ query, account }) => {
|
|
25300
|
-
return runOrDiagnose(["drive", "search", query], {
|
|
25418
|
+
}, async ({ query, view, account }) => {
|
|
25419
|
+
return runOrDiagnose(["drive", "search", query], {
|
|
25420
|
+
account,
|
|
25421
|
+
stripMedia: resolveView(view, ["compact", "full"]) === "compact"
|
|
25422
|
+
});
|
|
25301
25423
|
});
|
|
25302
25424
|
server.registerTool("gog_drive_get", {
|
|
25303
25425
|
description: "Get metadata for a Google Drive file.",
|
|
25304
25426
|
annotations: { readOnlyHint: true },
|
|
25305
25427
|
inputSchema: {
|
|
25306
25428
|
fileId: external_exports.string().describe("File ID"),
|
|
25429
|
+
// A --fields mask saves only 7% here: the default set is already narrow,
|
|
25430
|
+
// which is why this tool takes no mask. The media strip saves 27.5% of
|
|
25431
|
+
// the tool's actual output, measured end to end over stdio, which is why
|
|
25432
|
+
// it takes a view after all. (An earlier note said 32.9%; that was a
|
|
25433
|
+
// minified-vs-stripped comparison of the raw gog payload rather than of
|
|
25434
|
+
// what the tool returns. The end-to-end figure is the one a caller sees.)
|
|
25435
|
+
view: viewParam(["compact", "full"], { note: "compact (the default) drops thumbnailLink \u2014 a URL a model cannot see, and 30%+ of a Drive file record. Ask for full to get it back." }),
|
|
25307
25436
|
account: accountParam
|
|
25308
25437
|
}
|
|
25309
|
-
}, async ({ fileId, account }) => {
|
|
25310
|
-
return runOrDiagnose(["drive", "get", fileId], {
|
|
25438
|
+
}, async ({ fileId, view, account }) => {
|
|
25439
|
+
return runOrDiagnose(["drive", "get", fileId], {
|
|
25440
|
+
account,
|
|
25441
|
+
stripMedia: resolveView(view, ["compact", "full"]) === "compact"
|
|
25442
|
+
});
|
|
25311
25443
|
});
|
|
25312
25444
|
server.registerTool("gog_drive_mkdir", {
|
|
25313
25445
|
description: "Create a new folder in Google Drive.",
|
|
@@ -26040,7 +26172,7 @@ function registerTasksTools(server) {
|
|
|
26040
26172
|
}
|
|
26041
26173
|
|
|
26042
26174
|
// src/server.ts
|
|
26043
|
-
var VERSION = true ? "2.
|
|
26175
|
+
var VERSION = true ? "2.29.0" : "0.0.0";
|
|
26044
26176
|
var BASE_TOOL_REGISTRARS = [
|
|
26045
26177
|
registerApiTools,
|
|
26046
26178
|
registerAppScriptTools,
|
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.29.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",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
},
|
|
127
127
|
{
|
|
128
128
|
"name": "gog_calendar_events",
|
|
129
|
-
"description": "List calendar events;
|
|
129
|
+
"description": "List calendar events; compact by default (view=full for descriptions and attendees), paginated and flags a truncated range"
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
"name": "gog_calendar_get",
|
|
@@ -254,11 +254,11 @@
|
|
|
254
254
|
},
|
|
255
255
|
{
|
|
256
256
|
"name": "gog_drive_ls",
|
|
257
|
-
"description": "List files in a Google Drive folder"
|
|
257
|
+
"description": "List files in a Google Drive folder; compact by default (view=full for owners, parents and thumbnails)"
|
|
258
258
|
},
|
|
259
259
|
{
|
|
260
260
|
"name": "gog_drive_search",
|
|
261
|
-
"description": "Search Google Drive files"
|
|
261
|
+
"description": "Search Google Drive files; compact by default (view=full for thumbnails)"
|
|
262
262
|
},
|
|
263
263
|
{
|
|
264
264
|
"name": "gog_drive_extract_text",
|
|
@@ -270,7 +270,7 @@
|
|
|
270
270
|
},
|
|
271
271
|
{
|
|
272
272
|
"name": "gog_drive_get",
|
|
273
|
-
"description": "Get Google Drive file metadata"
|
|
273
|
+
"description": "Get Google Drive file metadata; compact by default (view=full for thumbnails)"
|
|
274
274
|
},
|
|
275
275
|
{
|
|
276
276
|
"name": "gog_drive_mkdir",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.29.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp",
|
|
5
5
|
"description": "MCP server wrapping gogcli for Google service access",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"test:coverage": "vitest run --coverage"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@chrischall/mcp-utils": "^0.
|
|
44
|
+
"@chrischall/mcp-utils": "^0.23.0",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
46
46
|
"zod": "^4.4.3"
|
|
47
47
|
},
|
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.29.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.29.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|