gogcli-mcp 2.27.1 → 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/README.md +3 -3
- package/SKILL.md +2 -2
- package/dist/index.js +171 -15
- package/dist/lib.js +172 -16
- package/manifest.json +9 -5
- package/mint.yaml +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/runner.ts +1 -1
- package/src/tools/calendar.ts +32 -2
- package/src/tools/chat.ts +36 -1
- 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/chat.test.ts +61 -1
- package/tests/tools/drive.test.ts +81 -13
- package/tests/tools/utils.test.ts +141 -0
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 }] };
|
|
@@ -23325,7 +23396,7 @@ function activeExecutor() {
|
|
|
23325
23396
|
return runExecutor.getStore() ?? defaultExecutor;
|
|
23326
23397
|
}
|
|
23327
23398
|
var TIMEOUT_MS = 3e4;
|
|
23328
|
-
var MIN_GOG_VERSION = "0.
|
|
23399
|
+
var MIN_GOG_VERSION = "0.39.0";
|
|
23329
23400
|
function readonlyEnvEnabled() {
|
|
23330
23401
|
return readEnvVar("GOG_READONLY") !== void 0 && parseBoolEnv("GOG_READONLY", { default: true });
|
|
23331
23402
|
}
|
|
@@ -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", {
|
|
@@ -24604,6 +24711,30 @@ function registerChatTools(server) {
|
|
|
24604
24711
|
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
24605
24712
|
return runOrDiagnose(args, { account });
|
|
24606
24713
|
});
|
|
24714
|
+
server.registerTool("gog_chat_messages_search", {
|
|
24715
|
+
description: 'Search Chat messages ACROSS every space and DM the account can see (gog >= 0.39.0) \u2014 the tool to reach for when the user asks "where did we discuss X" without naming a room, since gog_chat_messages_list needs a space up front. The query is Google Chat filter syntax, so it takes plain keywords or filters like sender, space, date, mention, unread, link and attachment. It is a search, NOT an export: Chat excludes some conversations (muted spaces among them), so an empty result does not prove a message never existed. view="full" adds each hit\'s read state and space mute setting; read state works on an ordinary chat grant, but the mute setting needs chat.users.spacesettings, which gog\'s chat scope set does NOT request (re-auth with extraScopes to get it). Missing metadata is OMITTED rather than defaulted, so an absent `read` means unknown while an explicit false means unread.' + workspaceOnlyNote,
|
|
24716
|
+
annotations: { readOnlyHint: true },
|
|
24717
|
+
inputSchema: {
|
|
24718
|
+
query: external_exports.string().describe('Google Chat filter-syntax query \u2014 keywords, or filters such as "from:alice@example.com budget"'),
|
|
24719
|
+
order: external_exports.enum(["create_time desc", "relevance desc"]).optional().describe(
|
|
24720
|
+
`Sort order. NOTE the snake_case, which differs from gog_chat_messages_list's camelCase. "relevance desc" needs Google Developer Preview access and errors without it`
|
|
24721
|
+
),
|
|
24722
|
+
view: external_exports.enum(["basic", "full"]).optional().describe(
|
|
24723
|
+
'Result view (default "basic"). "full" also requests read state (covered by an ordinary chat grant) and space mute setting (needs chat.users.spacesettings, which that grant does not include)'
|
|
24724
|
+
),
|
|
24725
|
+
markup: external_exports.enum(["chat", "markdown"]).optional().describe("Syntax to render each hit's formatted text in"),
|
|
24726
|
+
...paginationParams,
|
|
24727
|
+
max: external_exports.number().int().min(1).max(100).optional().describe("Max results per page (1-100; Chat search caps a page at 100)"),
|
|
24728
|
+
account: accountParam
|
|
24729
|
+
}
|
|
24730
|
+
}, async ({ query, order, view, markup, max, pageToken, page, all, account }) => {
|
|
24731
|
+
const args = ["chat", "messages", "search", query];
|
|
24732
|
+
if (order) args.push(`--order=${order}`);
|
|
24733
|
+
if (view) args.push(`--view=${view}`);
|
|
24734
|
+
if (markup) args.push(`--markup=${markup}`);
|
|
24735
|
+
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
24736
|
+
return runOrDiagnose(args, { account });
|
|
24737
|
+
});
|
|
24607
24738
|
server.registerTool("gog_chat_messages_send", {
|
|
24608
24739
|
description: "Post a message to a Chat space. THIS IS IMMEDIATELY VISIBLE TO EVERYONE IN THE SPACE and cannot be unsent through this tool, so treat it like sending mail, not like saving a draft. Pass `thread` to reply inside an existing conversation (from gog_chat_threads_list or a message's thread field); omit it to start a new one. Text supports Chat's markdown-ish formatting (*bold*, _italic_, `code`)." + workspaceOnlyNote,
|
|
24609
24740
|
inputSchema: {
|
|
@@ -25242,6 +25373,7 @@ function fileMeta(raw) {
|
|
|
25242
25373
|
const f = parsed.file ?? parsed;
|
|
25243
25374
|
return { name: f.name, mimeType: f.mimeType };
|
|
25244
25375
|
}
|
|
25376
|
+
var DRIVE_LS_COMPACT_FIELDS = "nextPageToken,files(id,name,mimeType,modifiedTime,size,webViewLink)";
|
|
25245
25377
|
function registerDriveTools(server) {
|
|
25246
25378
|
server.registerTool("gog_drive_ls", {
|
|
25247
25379
|
description: "List files in a Google Drive folder (default: root).",
|
|
@@ -25253,9 +25385,12 @@ function registerDriveTools(server) {
|
|
|
25253
25385
|
page: pageAliasParam,
|
|
25254
25386
|
query: external_exports.string().optional().describe(`Drive query filter (e.g. "name contains 'budget'")`),
|
|
25255
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
|
+
}),
|
|
25256
25391
|
account: accountParam
|
|
25257
25392
|
}
|
|
25258
|
-
}, async ({ folderId, max, pageToken, page, query, allDrives, account }) => {
|
|
25393
|
+
}, async ({ folderId, max, pageToken, page, query, allDrives, view, account }) => {
|
|
25259
25394
|
const args = ["drive", "ls"];
|
|
25260
25395
|
if (folderId) args.push(`--parent=${folderId}`);
|
|
25261
25396
|
if (max !== void 0) args.push(`--max=${max}`);
|
|
@@ -25263,27 +25398,48 @@ function registerDriveTools(server) {
|
|
|
25263
25398
|
if (token) args.push(`--page=${token}`);
|
|
25264
25399
|
if (query) args.push(`--query=${query}`);
|
|
25265
25400
|
if (allDrives === false) args.push("--no-all-drives");
|
|
25266
|
-
|
|
25401
|
+
const rung = resolveView(view, ["compact", "full"]);
|
|
25402
|
+
return runOrDiagnose(args, {
|
|
25403
|
+
account,
|
|
25404
|
+
fieldsMask: rung === "compact" ? DRIVE_LS_COMPACT_FIELDS : void 0
|
|
25405
|
+
});
|
|
25267
25406
|
});
|
|
25268
25407
|
server.registerTool("gog_drive_search", {
|
|
25269
25408
|
description: "Search Google Drive files by full-text query.",
|
|
25270
25409
|
annotations: { readOnlyHint: true },
|
|
25271
25410
|
inputSchema: {
|
|
25272
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." }),
|
|
25273
25416
|
account: accountParam
|
|
25274
25417
|
}
|
|
25275
|
-
}, async ({ query, account }) => {
|
|
25276
|
-
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
|
+
});
|
|
25277
25423
|
});
|
|
25278
25424
|
server.registerTool("gog_drive_get", {
|
|
25279
25425
|
description: "Get metadata for a Google Drive file.",
|
|
25280
25426
|
annotations: { readOnlyHint: true },
|
|
25281
25427
|
inputSchema: {
|
|
25282
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." }),
|
|
25283
25436
|
account: accountParam
|
|
25284
25437
|
}
|
|
25285
|
-
}, async ({ fileId, account }) => {
|
|
25286
|
-
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
|
+
});
|
|
25287
25443
|
});
|
|
25288
25444
|
server.registerTool("gog_drive_mkdir", {
|
|
25289
25445
|
description: "Create a new folder in Google Drive.",
|
|
@@ -26016,7 +26172,7 @@ function registerTasksTools(server) {
|
|
|
26016
26172
|
}
|
|
26017
26173
|
|
|
26018
26174
|
// src/server.ts
|
|
26019
|
-
var VERSION = true ? "2.
|
|
26175
|
+
var VERSION = true ? "2.29.0" : "0.0.0";
|
|
26020
26176
|
var BASE_TOOL_REGISTRARS = [
|
|
26021
26177
|
registerApiTools,
|
|
26022
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",
|
|
@@ -452,6 +452,10 @@
|
|
|
452
452
|
"name": "gog_chat_messages_list",
|
|
453
453
|
"description": "Read messages in a space, with mentions and reaction summaries"
|
|
454
454
|
},
|
|
455
|
+
{
|
|
456
|
+
"name": "gog_chat_messages_search",
|
|
457
|
+
"description": "Search Chat messages across every space and DM the account can see"
|
|
458
|
+
},
|
|
455
459
|
{
|
|
456
460
|
"name": "gog_chat_messages_send",
|
|
457
461
|
"description": "Post a message to a Chat space (immediately visible; supports attachments)"
|
package/mint.yaml
CHANGED
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
|
},
|
package/src/runner.ts
CHANGED
|
@@ -217,7 +217,7 @@ const TIMEOUT_MS = 30_000;
|
|
|
217
217
|
// so the requirement change is surfaced in the release notes (see
|
|
218
218
|
// .github/release.yml). This is the single source of truth for the required
|
|
219
219
|
// version; keep the README/CLAUDE.md mention in sync.
|
|
220
|
-
export const MIN_GOG_VERSION = '0.
|
|
220
|
+
export const MIN_GOG_VERSION = '0.39.0';
|
|
221
221
|
|
|
222
222
|
// Interpret the GOG_READONLY kill-switch. `readEnvVar` already treats blank
|
|
223
223
|
// values, 'undefined'/'null' sentinels, and unresolved .mcpb placeholders
|
package/src/tools/calendar.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { viewParam, resolveView } from '@chrischall/mcp-utils';
|
|
3
4
|
import { accountParam, runOrDiagnose, registerRunTool, pageTokenParam, pageAliasParam, resolvePageToken } from './utils.js';
|
|
4
5
|
import { annotateTruncatedList } from '../pagination.js';
|
|
5
6
|
|
|
@@ -53,6 +54,27 @@ function pushReminderFlags(
|
|
|
53
54
|
for (const reminder of p.reminders) args.push(`--reminder=${reminder}`);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
|
|
58
|
+
// The `compact` rung for gog_calendar_events, as a Google Calendar field mask.
|
|
59
|
+
//
|
|
60
|
+
// nextPageToken FIRST and always. This tool's own description tells a caller
|
|
61
|
+
// that a wide range is usually incomplete and to page until the cursor is gone;
|
|
62
|
+
// a mask of `items(...)` alone drops that cursor from the envelope, which would
|
|
63
|
+
// turn that instruction into a guarantee of the wrong answer. Verified live
|
|
64
|
+
// against gog 0.39.0.
|
|
65
|
+
//
|
|
66
|
+
// Chosen from the DATA over a 25-event window: description costs 5,951 bytes
|
|
67
|
+
// and attendees 3,145 — the two fat blobs `full` exists to return — while etag,
|
|
68
|
+
// kind, iCalUID, eventType, timezone, guestsCanInviteOthers and privateCopy are
|
|
69
|
+
// internal or single-valued across every row. Net: 21,260 -> 7,292 bytes, 66%
|
|
70
|
+
// smaller. status is kept despite being single-valued in that sample precisely
|
|
71
|
+
// because its whole value is flagging the rare cancelled event.
|
|
72
|
+
//
|
|
73
|
+
// gog's derived fields (startLocal, endDayOfWeek, ...) survive the mask, since
|
|
74
|
+
// gog computes them from start/end, which the mask keeps.
|
|
75
|
+
export const CALENDAR_EVENTS_COMPACT_FIELDS =
|
|
76
|
+
'nextPageToken,items(id,summary,start,end,location,status,htmlLink)';
|
|
77
|
+
|
|
56
78
|
export function registerCalendarTools(server: McpServer): void {
|
|
57
79
|
server.registerTool('gog_calendar_events', {
|
|
58
80
|
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): '
|
|
@@ -78,9 +100,13 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
78
100
|
all: z.boolean().optional().describe('Fetch events from ALL CALENDARS. NOTE: unlike the gmail search tools, this does NOT mean "all pages" — it widens the calendar set, not the page window. Use pageToken to reach later pages.'),
|
|
79
101
|
eventTypes: z.array(z.enum(['default', 'birthday', 'focus-time', 'from-gmail', 'out-of-office', 'working-location'])).optional().describe('Filter to specific event types (repeatable)'),
|
|
80
102
|
timezone: z.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.'),
|
|
103
|
+
view: viewParam(['compact', 'full'], {
|
|
104
|
+
note: 'compact (the default) drops description and attendees — together two thirds of a '
|
|
105
|
+
+ 'listing\'s bytes — plus etag/iCalUID/kind. Ask for full when you need a body or a guest list.',
|
|
106
|
+
}),
|
|
81
107
|
account: accountParam,
|
|
82
108
|
},
|
|
83
|
-
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
109
|
+
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, view, account }) => {
|
|
84
110
|
const args = ['calendar', 'events'];
|
|
85
111
|
if (calendarId) args.push(calendarId);
|
|
86
112
|
if (from) args.push(`--from=${from}`);
|
|
@@ -94,7 +120,11 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
94
120
|
if (all) args.push('--all');
|
|
95
121
|
if (eventTypes) for (const t of eventTypes) args.push(`--event-types=${t}`);
|
|
96
122
|
if (timezone) args.push(`--timezone=${timezone}`);
|
|
97
|
-
const
|
|
123
|
+
const rung = resolveView(view, ['compact', 'full']);
|
|
124
|
+
const result = await runOrDiagnose(args, {
|
|
125
|
+
account,
|
|
126
|
+
fieldsMask: rung === 'compact' ? CALENDAR_EVENTS_COMPACT_FIELDS : undefined,
|
|
127
|
+
});
|
|
98
128
|
// No count probe here: unlike Gmail's list endpoints, the Calendar API has
|
|
99
129
|
// no cheap way to count a range exactly, so the warning carries the fact of
|
|
100
130
|
// truncation without inventing a total.
|
package/src/tools/chat.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { GogArg } from '../runner.js';
|
|
|
11
11
|
import { attachInlineParam, inlineAttachmentArgs } from '../attachments.js';
|
|
12
12
|
|
|
13
13
|
// Google Chat (gog >= 0.38.0 for the mention/reaction metadata in
|
|
14
|
-
// `messages list
|
|
14
|
+
// `messages list`, >= 0.39.0 for `messages search`; the rest is older).
|
|
15
15
|
//
|
|
16
16
|
// TWO NAMING SYSTEMS MEET HERE, and mixing them is the mistake this module's
|
|
17
17
|
// descriptions exist to prevent. Chat identifies everything by RESOURCE NAME —
|
|
@@ -135,6 +135,41 @@ export function registerChatTools(server: McpServer): void {
|
|
|
135
135
|
return runOrDiagnose(args, { account });
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
server.registerTool('gog_chat_messages_search', {
|
|
139
|
+
description:
|
|
140
|
+
'Search Chat messages ACROSS every space and DM the account can see (gog >= 0.39.0) — the tool to reach for when the '
|
|
141
|
+
+ 'user asks "where did we discuss X" without naming a room, since gog_chat_messages_list needs a space up front. '
|
|
142
|
+
+ 'The query is Google Chat filter syntax, so it takes plain keywords or filters like sender, space, date, mention, '
|
|
143
|
+
+ 'unread, link and attachment. It is a search, NOT an export: Chat excludes some conversations (muted spaces among '
|
|
144
|
+
+ 'them), so an empty result does not prove a message never existed. view="full" adds each hit\'s read state and space '
|
|
145
|
+
+ 'mute setting; read state works on an ordinary chat grant, but the mute setting needs chat.users.spacesettings, '
|
|
146
|
+
+ 'which gog\'s chat scope set does NOT request (re-auth with extraScopes to get it). Missing metadata is OMITTED '
|
|
147
|
+
+ 'rather than defaulted, so an absent `read` means unknown while an explicit false means unread.' + workspaceOnlyNote,
|
|
148
|
+
annotations: { readOnlyHint: true },
|
|
149
|
+
inputSchema: {
|
|
150
|
+
query: z.string().describe('Google Chat filter-syntax query — keywords, or filters such as "from:alice@example.com budget"'),
|
|
151
|
+
order: z.enum(['create_time desc', 'relevance desc']).optional().describe(
|
|
152
|
+
'Sort order. NOTE the snake_case, which differs from gog_chat_messages_list\'s camelCase. "relevance desc" needs '
|
|
153
|
+
+ 'Google Developer Preview access and errors without it',
|
|
154
|
+
),
|
|
155
|
+
view: z.enum(['basic', 'full']).optional().describe(
|
|
156
|
+
'Result view (default "basic"). "full" also requests read state (covered by an ordinary chat grant) and space '
|
|
157
|
+
+ 'mute setting (needs chat.users.spacesettings, which that grant does not include)',
|
|
158
|
+
),
|
|
159
|
+
markup: z.enum(['chat', 'markdown']).optional().describe('Syntax to render each hit\'s formatted text in'),
|
|
160
|
+
...paginationParams,
|
|
161
|
+
max: z.number().int().min(1).max(100).optional().describe('Max results per page (1-100; Chat search caps a page at 100)'),
|
|
162
|
+
account: accountParam,
|
|
163
|
+
},
|
|
164
|
+
}, async ({ query, order, view, markup, max, pageToken, page, all, account }) => {
|
|
165
|
+
const args = ['chat', 'messages', 'search', query];
|
|
166
|
+
if (order) args.push(`--order=${order}`);
|
|
167
|
+
if (view) args.push(`--view=${view}`);
|
|
168
|
+
if (markup) args.push(`--markup=${markup}`);
|
|
169
|
+
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
170
|
+
return runOrDiagnose(args, { account });
|
|
171
|
+
});
|
|
172
|
+
|
|
138
173
|
server.registerTool('gog_chat_messages_send', {
|
|
139
174
|
description:
|
|
140
175
|
'Post a message to a Chat space. THIS IS IMMEDIATELY VISIBLE TO EVERYONE IN THE SPACE and cannot be unsent through '
|
package/src/tools/drive.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { rawTextResult } from '@chrischall/mcp-utils';
|
|
4
|
+
import { rawTextResult, viewParam, resolveView } from '@chrischall/mcp-utils';
|
|
5
|
+
|
|
5
6
|
import { run, runBinary } from '../runner.js';
|
|
6
7
|
import { accountParam, diagnose, runOrDiagnose, registerRunTool, pageTokenParam, pageAliasParam, resolvePageToken} from './utils.js';
|
|
7
8
|
|
|
@@ -18,6 +19,21 @@ function fileMeta(raw: string): { name?: string; mimeType?: string } {
|
|
|
18
19
|
return { name: f.name, mimeType: f.mimeType };
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
|
|
23
|
+
// The `compact` rung for gog_drive_ls, as a Google Drive field mask.
|
|
24
|
+
//
|
|
25
|
+
// nextPageToken FIRST and always: a mask of `files(...)` alone drops the cursor
|
|
26
|
+
// from the envelope, and a compact listing would then look complete when it was
|
|
27
|
+
// one page of many. Verified live against gog 0.39.0.
|
|
28
|
+
//
|
|
29
|
+
// The dropped fields were chosen from the DATA, not from taste — measured over
|
|
30
|
+
// a 25-row listing: thumbnailLink costs 4,998 bytes at ONE distinct value,
|
|
31
|
+
// owners 1,400 at one, parents 900 at one, hasThumbnail 550 at one. Everything
|
|
32
|
+
// kept below varies across rows. Net: 15,718 -> 8,095 bytes, 48% smaller.
|
|
33
|
+
// A caller who needs an owner or a parent asks for view="full".
|
|
34
|
+
export const DRIVE_LS_COMPACT_FIELDS =
|
|
35
|
+
'nextPageToken,files(id,name,mimeType,modifiedTime,size,webViewLink)';
|
|
36
|
+
|
|
21
37
|
export function registerDriveTools(server: McpServer): void {
|
|
22
38
|
server.registerTool('gog_drive_ls', {
|
|
23
39
|
description: 'List files in a Google Drive folder (default: root).',
|
|
@@ -29,9 +45,13 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
29
45
|
page: pageAliasParam,
|
|
30
46
|
query: z.string().optional().describe('Drive query filter (e.g. "name contains \'budget\'")'),
|
|
31
47
|
allDrives: z.boolean().optional().describe('Include shared drives (default: true). Set false for My Drive only.'),
|
|
48
|
+
view: viewParam(['compact', 'full'], {
|
|
49
|
+
note: 'compact (the default) drops owners, parents, thumbnailLink and hasThumbnail — '
|
|
50
|
+
+ 'near-constant across a listing and 48% of its bytes. Ask for full to get them.',
|
|
51
|
+
}),
|
|
32
52
|
account: accountParam,
|
|
33
53
|
},
|
|
34
|
-
}, async ({ folderId, max, pageToken, page, query, allDrives, account }) => {
|
|
54
|
+
}, async ({ folderId, max, pageToken, page, query, allDrives, view, account }) => {
|
|
35
55
|
const args = ['drive', 'ls'];
|
|
36
56
|
if (folderId) args.push(`--parent=${folderId}`);
|
|
37
57
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
@@ -39,7 +59,11 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
39
59
|
if (token) args.push(`--page=${token}`);
|
|
40
60
|
if (query) args.push(`--query=${query}`);
|
|
41
61
|
if (allDrives === false) args.push('--no-all-drives');
|
|
42
|
-
|
|
62
|
+
const rung = resolveView(view, ['compact', 'full']);
|
|
63
|
+
return runOrDiagnose(args, {
|
|
64
|
+
account,
|
|
65
|
+
fieldsMask: rung === 'compact' ? DRIVE_LS_COMPACT_FIELDS : undefined,
|
|
66
|
+
});
|
|
43
67
|
});
|
|
44
68
|
|
|
45
69
|
server.registerTool('gog_drive_search', {
|
|
@@ -47,10 +71,17 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
47
71
|
annotations: { readOnlyHint: true },
|
|
48
72
|
inputSchema: {
|
|
49
73
|
query: z.string().describe('Search query'),
|
|
74
|
+
// gog's `drive search` accepts no --fields mask, so unlike gog_drive_ls
|
|
75
|
+
// this tool's compact rung is a LOCAL projection. Same vocabulary either
|
|
76
|
+
// way: a caller does not need to know which lever is being pulled.
|
|
77
|
+
view: viewParam(['compact', 'full'], { note: 'compact (the default) drops thumbnailLink — a URL a model cannot see, and 30%+ of a Drive file record. Ask for full to get it back.' }),
|
|
50
78
|
account: accountParam,
|
|
51
79
|
},
|
|
52
|
-
}, async ({ query, account }) => {
|
|
53
|
-
return runOrDiagnose(['drive', 'search', query], {
|
|
80
|
+
}, async ({ query, view, account }) => {
|
|
81
|
+
return runOrDiagnose(['drive', 'search', query], {
|
|
82
|
+
account,
|
|
83
|
+
stripMedia: resolveView(view, ['compact', 'full']) === 'compact',
|
|
84
|
+
});
|
|
54
85
|
});
|
|
55
86
|
|
|
56
87
|
server.registerTool('gog_drive_get', {
|
|
@@ -58,10 +89,20 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
58
89
|
annotations: { readOnlyHint: true },
|
|
59
90
|
inputSchema: {
|
|
60
91
|
fileId: z.string().describe('File ID'),
|
|
92
|
+
// A --fields mask saves only 7% here: the default set is already narrow,
|
|
93
|
+
// which is why this tool takes no mask. The media strip saves 27.5% of
|
|
94
|
+
// the tool's actual output, measured end to end over stdio, which is why
|
|
95
|
+
// it takes a view after all. (An earlier note said 32.9%; that was a
|
|
96
|
+
// minified-vs-stripped comparison of the raw gog payload rather than of
|
|
97
|
+
// what the tool returns. The end-to-end figure is the one a caller sees.)
|
|
98
|
+
view: viewParam(['compact', 'full'], { note: 'compact (the default) drops thumbnailLink — a URL a model cannot see, and 30%+ of a Drive file record. Ask for full to get it back.' }),
|
|
61
99
|
account: accountParam,
|
|
62
100
|
},
|
|
63
|
-
}, async ({ fileId, account }) => {
|
|
64
|
-
return runOrDiagnose(['drive', 'get', fileId], {
|
|
101
|
+
}, async ({ fileId, view, account }) => {
|
|
102
|
+
return runOrDiagnose(['drive', 'get', fileId], {
|
|
103
|
+
account,
|
|
104
|
+
stripMedia: resolveView(view, ['compact', 'full']) === 'compact',
|
|
105
|
+
});
|
|
65
106
|
});
|
|
66
107
|
|
|
67
108
|
server.registerTool('gog_drive_mkdir', {
|