gogcli-mcp 2.0.11 → 2.2.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.
@@ -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.0.11"
10
+ "version": "2.2.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.0.11",
18
+ "version": "2.2.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp",
3
3
  "displayName": "gogcli",
4
- "version": "2.0.11",
4
+ "version": "2.2.0",
5
5
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ ## [2.1.0](https://github.com/chrischall/gogcli-mcp/compare/v2.0.13...v2.1.0) (2026-05-25)
4
+
5
+
6
+ ### Features
7
+
8
+ * add gogcli-mcp-slides and gogcli-mcp-classroom sub-packages ([cee8724](https://github.com/chrischall/gogcli-mcp/commit/cee872442240e52ae1cac9b9085eabce0c5ff9c3))
9
+ * add manifest.json and SKILL.md to sub-packages for .mcpb and .skill builds ([d1339c6](https://github.com/chrischall/gogcli-mcp/commit/d1339c690b1eeee8221b7cc15fd9e4fa48c1e20a))
10
+ * address open issues for docs + sheets ([7109253](https://github.com/chrischall/gogcli-mcp/commit/7109253b3eb4b3dcc17cda7a8dfb1b6969023bcf))
11
+ * **deploy:** per-sub-package registry listings (MCP Registry, ClawHub, Claude plugins) ([6cc1798](https://github.com/chrischall/gogcli-mcp/commit/6cc1798320af6dd3af709790e6bec30f46c5f3d2))
12
+ * **drive:** add gogcli-mcp-drive sub-package ([d8eef61](https://github.com/chrischall/gogcli-mcp/commit/d8eef61f16266209da70cd05ca0d2378bf3c4613))
13
+ * fold People into contacts and Meet into calendar; add TODO.md ([f649119](https://github.com/chrischall/gogcli-mcp/commit/f64911902094f87ae31420913e9d1a5ba608441f))
14
+ * wrap new gog cli v0.18.0 tools ([173242a](https://github.com/chrischall/gogcli-mcp/commit/173242aafaa8c2f2d7282a2fbd8faedfc65a63bd))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * emit type declarations from base so sub-packages can resolve gogcli-mcp/lib ([9cee5ed](https://github.com/chrischall/gogcli-mcp/commit/9cee5ed97b0cc72dde81c7f8a01d71c95a0291b7))
20
+ * **mcpb:** add per-sub-package .mcpbignore to trim bundles ([e739d9e](https://github.com/chrischall/gogcli-mcp/commit/e739d9ec9ae8e4cc19b1189bb5bf7ac90481c943))
21
+ * **runner:** augment PATH + helpful ENOENT message for missing gog ([34e696f](https://github.com/chrischall/gogcli-mcp/commit/34e696f8c74c7003e6b1bb31991a05184a3b6094))
22
+ * **runner:** fall back to PATH lookup when GOG_PATH is empty string ([a726592](https://github.com/chrischall/gogcli-mcp/commit/a726592c86e869fe27b176798a9b8909e7821f4c))
23
+ * **runner:** treat unresolved .mcpb placeholders as unset env vars ([17a0363](https://github.com/chrischall/gogcli-mcp/commit/17a0363f7dffd2757667e0d1d9377e2e2cb6fbc7))
24
+ * treat empty GOG_PATH as unset, fall back to 'gog' on PATH ([0204638](https://github.com/chrischall/gogcli-mcp/commit/020463813f941b1fa8ec2360346c55abc244675f))
25
+
26
+
27
+ ### Refactor
28
+
29
+ * aggressive cleanup pass (audit findings) ([e635b39](https://github.com/chrischall/gogcli-mcp/commit/e635b391cf611e9c102a422ffbce5ebe28687b80))
30
+ * make sub-packages focused (auth + service only) ([4301306](https://github.com/chrischall/gogcli-mcp/commit/4301306cecf12e66d67dbc4ce9debae8f3dcfabc))
31
+ * rebalance base vs sub-package tool split (120 -> 84 base) ([1074953](https://github.com/chrischall/gogcli-mcp/commit/1074953833fc4766e85cc0b28cbcc08834f8dc47))
32
+ * remove comments escape hatch from base docs tool ([a5364c6](https://github.com/chrischall/gogcli-mcp/commit/a5364c6df1948e57b185b9d753aa11b033edd0d0))
33
+ * restructure into npm workspaces monorepo ([9645100](https://github.com/chrischall/gogcli-mcp/commit/9645100e8120c03956ee972023b12d2e99876cc5))
34
+ * review-pass cleanup (enums, shared schemas, test harness) ([ea851de](https://github.com/chrischall/gogcli-mcp/commit/ea851deb30b9cfef3f07ed506a011fc363b34018))
35
+ * use relative imports instead of workspace symlinks ([a278d6f](https://github.com/chrischall/gogcli-mcp/commit/a278d6fb31be480571481e9d87e199d261a3f031))
36
+
37
+
38
+ ### Documentation
39
+
40
+ * add per-package READMEs, update root README and CLAUDE.md for monorepo ([19d547e](https://github.com/chrischall/gogcli-mcp/commit/19d547e5ceb14298a75ab297d0a9a11c6ea59e24))
41
+ * fix tool counts and stale descriptions across all docs ([3558c72](https://github.com/chrischall/gogcli-mcp/commit/3558c72fa5c3ee3dbc8a06e9d7fb51bf73ef77c7))
42
+ * update gogcli repo URLs to openclaw/gogcli (was steipete/gogcli) ([951a181](https://github.com/chrischall/gogcli-mcp/commit/951a1818ebc269f203aee723ccbea32c13817d8b))
package/dist/index.js CHANGED
@@ -31074,7 +31074,7 @@ async function run(args, options = {}) {
31074
31074
 
31075
31075
  // src/tools/utils.ts
31076
31076
  var accountParam = external_exports.string().optional().describe(
31077
- "Google account email to use (overrides GOG_ACCOUNT env var)"
31077
+ "Google account email to use, e.g. you@gmail.com \u2014 must be the full address, not a bare username. Overrides the GOG_ACCOUNT env var. Omit to use the single configured account."
31078
31078
  );
31079
31079
  var ids = {
31080
31080
  course: external_exports.string().describe("Course ID"),
@@ -31133,26 +31133,41 @@ function toError(err) {
31133
31133
  }
31134
31134
  var AUTH_ERROR_PATTERN = /\b(401|unauthorized|token.*(expired|revoked)|invalid_grant)\b/i;
31135
31135
  var TRANSIENT_ERROR_PATTERN = /\b429\b|\b5\d\d\b|\bquota\b|rateLimit|\bDEADLINE_EXCEEDED\b/i;
31136
+ var GRID_LIMIT_ERROR_PATTERN = /exceeds grid limits/i;
31136
31137
  var AUTH_HINT = "\n\nAuthentication may have expired. Use gog_auth_add to re-authorize the account. Ask the user if they would like to re-authenticate.";
31137
31138
  var TRANSIENT_HINT = "\n\nThis error is often transient. Retry the same call before trying a different approach (do not fall back to smaller writes or row-by-row operations).";
31139
+ var GRID_LIMIT_HINT = "\n\nThe target range is outside the sheet's current grid. Add the missing rows or columns first with gog_sheets_insert (dimension: rows or cols), then retry the write.";
31140
+ function formatAccountList(raw) {
31141
+ try {
31142
+ const parsed = JSON.parse(raw);
31143
+ if (Array.isArray(parsed?.accounts)) {
31144
+ return parsed.accounts.map((a) => a?.email).filter(Boolean).join("\n");
31145
+ }
31146
+ } catch {
31147
+ }
31148
+ return raw.trim();
31149
+ }
31150
+ async function diagnose(err) {
31151
+ const errText = toError(err).content[0].text;
31152
+ const isAuthError = AUTH_ERROR_PATTERN.test(errText);
31153
+ const isTransientError = !isAuthError && TRANSIENT_ERROR_PATTERN.test(errText);
31154
+ const isGridLimitError = GRID_LIMIT_ERROR_PATTERN.test(errText);
31155
+ const hint = isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : isGridLimitError ? GRID_LIMIT_HINT : "";
31156
+ try {
31157
+ const accounts = formatAccountList(await run(["auth", "list"]));
31158
+ return toText(`${errText}
31159
+
31160
+ Configured accounts:
31161
+ ${accounts || "(none)"}${hint}`);
31162
+ } catch {
31163
+ return toText(`${errText}${hint}`);
31164
+ }
31165
+ }
31138
31166
  async function runOrDiagnose(args, options) {
31139
31167
  try {
31140
31168
  return toText(await run(args, options));
31141
31169
  } catch (err) {
31142
- const base = toError(err);
31143
- const errText = base.content[0].text;
31144
- const isAuthError = AUTH_ERROR_PATTERN.test(errText);
31145
- const isTransientError = !isAuthError && TRANSIENT_ERROR_PATTERN.test(errText);
31146
- const hint = isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : "";
31147
- try {
31148
- const accounts = await run(["auth", "list"]);
31149
- return toText(`${errText}
31150
-
31151
- Configured accounts:
31152
- ${accounts}${hint}`);
31153
- } catch {
31154
- return toText(`${errText}${hint}`);
31155
- }
31170
+ return diagnose(err);
31156
31171
  }
31157
31172
  }
31158
31173
 
@@ -31254,7 +31269,7 @@ function registerCalendarTools(server2) {
31254
31269
  return runOrDiagnose(["calendar", "event", calendarId, eventId], { account });
31255
31270
  });
31256
31271
  server2.registerTool("gog_calendar_create", {
31257
- description: "Create a calendar event.",
31272
+ description: "Create a calendar event. Set withZoom=true to attach a Zoom meeting (requires Zoom S2S OAuth setup via gog_zoom_auth_setup; the join URL + meeting ID + passcode are appended to the event description \u2014 Google rejects native conference card writes from non-Workspace-Marketplace OAuth clients).",
31258
31273
  annotations: { destructiveHint: false },
31259
31274
  inputSchema: {
31260
31275
  calendarId: external_exports.string().describe('Calendar ID (use "primary" for the default calendar)'),
@@ -31265,18 +31280,20 @@ function registerCalendarTools(server2) {
31265
31280
  location: external_exports.string().optional().describe("Event location"),
31266
31281
  attendees: external_exports.string().optional().describe("Attendee emails, comma-separated"),
31267
31282
  allDay: external_exports.boolean().optional().describe("All-day event (use date-only in from/to)"),
31283
+ withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)"),
31268
31284
  account: accountParam
31269
31285
  }
31270
- }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, account }) => {
31286
+ }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, withZoom, account }) => {
31271
31287
  const args = ["calendar", "create", calendarId, `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
31272
31288
  if (description) args.push(`--description=${description}`);
31273
31289
  if (location) args.push(`--location=${location}`);
31274
31290
  if (attendees) args.push(`--attendees=${attendees}`);
31275
31291
  if (allDay) args.push("--all-day");
31292
+ if (withZoom) args.push("--with-zoom");
31276
31293
  return runOrDiagnose(args, { account });
31277
31294
  });
31278
31295
  server2.registerTool("gog_calendar_update", {
31279
- description: "Update an existing calendar event.",
31296
+ description: "Update an existing calendar event. Zoom: withZoom adds a Zoom meeting, regenerateZoom replaces the existing one, removeZoom strips it (each are independent \u2014 use one per call).",
31280
31297
  annotations: { destructiveHint: false },
31281
31298
  inputSchema: {
31282
31299
  calendarId: external_exports.string().describe("Calendar ID"),
@@ -31287,9 +31304,12 @@ function registerCalendarTools(server2) {
31287
31304
  description: external_exports.string().optional().describe("New description"),
31288
31305
  location: external_exports.string().optional().describe("New location"),
31289
31306
  attendees: external_exports.string().optional().describe("New attendee emails, comma-separated (replaces existing)"),
31307
+ withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event"),
31308
+ regenerateZoom: external_exports.boolean().optional().describe("Replace the event's existing Zoom video conference"),
31309
+ removeZoom: external_exports.boolean().optional().describe("Remove the event's Zoom video conference"),
31290
31310
  account: accountParam
31291
31311
  }
31292
- }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, account }) => {
31312
+ }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, withZoom, regenerateZoom, removeZoom, account }) => {
31293
31313
  const args = ["calendar", "update", calendarId, eventId];
31294
31314
  if (summary !== void 0) args.push(`--summary=${summary}`);
31295
31315
  if (from !== void 0) args.push(`--from=${from}`);
@@ -31297,6 +31317,9 @@ function registerCalendarTools(server2) {
31297
31317
  if (description !== void 0) args.push(`--description=${description}`);
31298
31318
  if (location !== void 0) args.push(`--location=${location}`);
31299
31319
  if (attendees !== void 0) args.push(`--attendees=${attendees}`);
31320
+ if (withZoom) args.push("--with-zoom");
31321
+ if (regenerateZoom) args.push("--regenerate-zoom");
31322
+ if (removeZoom) args.push("--remove-zoom");
31300
31323
  return runOrDiagnose(args, { account });
31301
31324
  });
31302
31325
  server2.registerTool("gog_calendar_delete", {
@@ -31933,7 +31956,7 @@ function registerDriveTools(server2) {
31933
31956
  // src/tools/gmail.ts
31934
31957
  function registerGmailTools(server2) {
31935
31958
  server2.registerTool("gog_gmail_search", {
31936
- description: 'Search Gmail threads using Gmail query syntax (e.g. "from:alice subject:invoice is:unread").',
31959
+ description: `Search Gmail threads using Gmail query syntax (e.g. "from:alice subject:invoice is:unread"). The query is passed verbatim to Gmail; a bare name token (from:alison) matches per Gmail's own heuristics, a full address (from:alison@example.com) is exact. To match a contact across several addresses, OR them: from:(a@x.com OR b@y.com).`,
31937
31960
  annotations: { readOnlyHint: true },
31938
31961
  inputSchema: {
31939
31962
  query: external_exports.string().describe("Gmail search query"),
@@ -31982,8 +32005,62 @@ function registerGmailTools(server2) {
31982
32005
  registerRunTool(server2, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
31983
32006
  }
31984
32007
 
32008
+ // src/tools/sheets-a1.ts
32009
+ function colToLetter(n) {
32010
+ let s = "";
32011
+ while (n > 0) {
32012
+ const rem = (n - 1) % 26;
32013
+ s = String.fromCharCode(65 + rem) + s;
32014
+ n = Math.floor((n - 1) / 26);
32015
+ }
32016
+ return s;
32017
+ }
32018
+ function letterToCol(s) {
32019
+ let n = 0;
32020
+ for (const ch of s.toUpperCase()) {
32021
+ n = n * 26 + (ch.charCodeAt(0) - 64);
32022
+ }
32023
+ return n;
32024
+ }
32025
+ function expandAnchorRange(range, rows, cols) {
32026
+ const bang = range.lastIndexOf("!");
32027
+ const sheet = bang >= 0 ? range.slice(0, bang + 1) : "";
32028
+ const cell = bang >= 0 ? range.slice(bang + 1) : range;
32029
+ const m = /^([A-Za-z]+)([0-9]+)$/.exec(cell);
32030
+ if (!m) return range;
32031
+ const startCol = letterToCol(m[1]);
32032
+ const startRow = parseInt(m[2], 10);
32033
+ const endCol = colToLetter(startCol + cols - 1);
32034
+ const endRow = startRow + rows - 1;
32035
+ return `${sheet}${m[1].toUpperCase()}${startRow}:${endCol}${endRow}`;
32036
+ }
32037
+ function countNonEmptyCells(getOutput) {
32038
+ let parsed;
32039
+ try {
32040
+ parsed = JSON.parse(getOutput);
32041
+ } catch {
32042
+ return -1;
32043
+ }
32044
+ const values = parsed?.values;
32045
+ if (!Array.isArray(values)) return 0;
32046
+ let count = 0;
32047
+ for (const row of values) {
32048
+ if (!Array.isArray(row)) continue;
32049
+ for (const cell of row) {
32050
+ if (cell !== null && String(cell).trim() !== "") count++;
32051
+ }
32052
+ }
32053
+ return count;
32054
+ }
32055
+
31985
32056
  // src/tools/sheets.ts
31986
32057
  var cellValueParam = external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]);
32058
+ var dryRunParam = external_exports.boolean().optional().describe(
32059
+ "Preview the operation without modifying the sheet (gog --dry-run): reports the intended actions and exits without writing."
32060
+ );
32061
+ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
32062
+ 'Safety guard against silent overwrites: before writing, read the target range and refuse the write if any target cell already holds data. Costs one extra read. Anchor ranges (e.g. "Sheet1!A1") are expanded to the full area your values will cover; explicit and named ranges are checked as-is.'
32063
+ );
31987
32064
  function registerSheetsTools(server2) {
31988
32065
  server2.registerTool("gog_sheets_get", {
31989
32066
  description: 'Read values from a Google Sheets range. Returns a JSON object with a "values" array of rows.',
@@ -32003,13 +32080,31 @@ function registerSheetsTools(server2) {
32003
32080
  spreadsheetId: external_exports.string().describe("Spreadsheet ID (from the URL)"),
32004
32081
  range: external_exports.string().describe("Top-left cell or range in A1 notation, e.g. Sheet1!A1"),
32005
32082
  values: external_exports.array(external_exports.array(cellValueParam)).describe('2D array of values (rows of columns). Cells may be string/number/boolean/null; strings starting with "=" are formulas.'),
32083
+ dry_run: dryRunParam,
32084
+ fail_if_not_empty: failIfNotEmptyParam,
32006
32085
  account: accountParam
32007
32086
  }
32008
- }, async ({ spreadsheetId, range, values, account }) => {
32009
- return runOrDiagnose(
32010
- ["sheets", "update", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`],
32011
- { account }
32012
- );
32087
+ }, async ({ spreadsheetId, range, values, account, dry_run, fail_if_not_empty }) => {
32088
+ const cols = values.reduce((max, row) => Math.max(max, row.length), 0);
32089
+ if (fail_if_not_empty && values.length > 0 && cols > 0) {
32090
+ const readRange = expandAnchorRange(range, values.length, cols);
32091
+ let existing;
32092
+ try {
32093
+ existing = await run(["sheets", "get", spreadsheetId, readRange], { account });
32094
+ } catch (err) {
32095
+ return diagnose(err);
32096
+ }
32097
+ const occupied = countNonEmptyCells(existing);
32098
+ if (occupied !== 0) {
32099
+ const detail = occupied < 0 ? "could not be verified as empty" : `already contains data in ${occupied} cell(s)`;
32100
+ return toText(
32101
+ `Write aborted (fail_if_not_empty): target range ${readRange} ${detail}. Re-run without fail_if_not_empty to overwrite, or clear it first with gog_sheets_clear.`
32102
+ );
32103
+ }
32104
+ }
32105
+ const args = ["sheets", "update", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`];
32106
+ if (dry_run) args.push("--dry-run");
32107
+ return runOrDiagnose(args, { account });
32013
32108
  });
32014
32109
  server2.registerTool("gog_sheets_append", {
32015
32110
  description: 'Append rows to a Google Sheet after the last row with data in the given range. Values may be strings, numbers, booleans, or null. Strings starting with "=" are interpreted as formulas.',
@@ -32018,13 +32113,13 @@ function registerSheetsTools(server2) {
32018
32113
  spreadsheetId: external_exports.string().describe("Spreadsheet ID (from the URL)"),
32019
32114
  range: external_exports.string().describe("Range indicating which sheet/columns to append to, e.g. Sheet1!A:C"),
32020
32115
  values: external_exports.array(external_exports.array(cellValueParam)).describe('2D array of rows to append. Cells may be string/number/boolean/null; strings starting with "=" are formulas.'),
32116
+ dry_run: dryRunParam,
32021
32117
  account: accountParam
32022
32118
  }
32023
- }, async ({ spreadsheetId, range, values, account }) => {
32024
- return runOrDiagnose(
32025
- ["sheets", "append", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`],
32026
- { account }
32027
- );
32119
+ }, async ({ spreadsheetId, range, values, account, dry_run }) => {
32120
+ const args = ["sheets", "append", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`];
32121
+ if (dry_run) args.push("--dry-run");
32122
+ return runOrDiagnose(args, { account });
32028
32123
  });
32029
32124
  server2.registerTool("gog_sheets_clear", {
32030
32125
  description: "Clear all values in a Google Sheets range (formatting is preserved).",
@@ -32032,13 +32127,16 @@ function registerSheetsTools(server2) {
32032
32127
  inputSchema: {
32033
32128
  spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
32034
32129
  range: external_exports.string().describe("Range in A1 notation to clear"),
32130
+ dry_run: dryRunParam,
32035
32131
  account: accountParam
32036
32132
  }
32037
- }, async ({ spreadsheetId, range, account }) => {
32038
- return runOrDiagnose(["sheets", "clear", spreadsheetId, range], { account });
32133
+ }, async ({ spreadsheetId, range, account, dry_run }) => {
32134
+ const args = ["sheets", "clear", spreadsheetId, range];
32135
+ if (dry_run) args.push("--dry-run");
32136
+ return runOrDiagnose(args, { account });
32039
32137
  });
32040
32138
  server2.registerTool("gog_sheets_metadata", {
32041
- description: "Get spreadsheet metadata: title, sheet tabs, named ranges, and other properties.",
32139
+ description: "Get spreadsheet metadata: title, named ranges, and per-tab properties including grid dimensions (gridProperties.rowCount / columnCount). Use this to learn a sheet's current size before writing \u2014 a write outside the grid fails.",
32042
32140
  annotations: { readOnlyHint: true },
32043
32141
  inputSchema: {
32044
32142
  spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
@@ -32224,7 +32322,7 @@ function registerTasksTools(server2) {
32224
32322
  }
32225
32323
 
32226
32324
  // src/server.ts
32227
- var VERSION = true ? "2.0.11" : "0.0.0";
32325
+ var VERSION = true ? "2.2.0" : "0.0.0";
32228
32326
  function createServer(options) {
32229
32327
  return new McpServer({
32230
32328
  name: options?.name ?? "gogcli",
package/dist/lib.js CHANGED
@@ -30981,7 +30981,7 @@ async function run(args, options = {}) {
30981
30981
 
30982
30982
  // src/tools/utils.ts
30983
30983
  var accountParam = external_exports.string().optional().describe(
30984
- "Google account email to use (overrides GOG_ACCOUNT env var)"
30984
+ "Google account email to use, e.g. you@gmail.com \u2014 must be the full address, not a bare username. Overrides the GOG_ACCOUNT env var. Omit to use the single configured account."
30985
30985
  );
30986
30986
  var ids = {
30987
30987
  course: external_exports.string().describe("Course ID"),
@@ -31045,26 +31045,41 @@ function toError(err) {
31045
31045
  }
31046
31046
  var AUTH_ERROR_PATTERN = /\b(401|unauthorized|token.*(expired|revoked)|invalid_grant)\b/i;
31047
31047
  var TRANSIENT_ERROR_PATTERN = /\b429\b|\b5\d\d\b|\bquota\b|rateLimit|\bDEADLINE_EXCEEDED\b/i;
31048
+ var GRID_LIMIT_ERROR_PATTERN = /exceeds grid limits/i;
31048
31049
  var AUTH_HINT = "\n\nAuthentication may have expired. Use gog_auth_add to re-authorize the account. Ask the user if they would like to re-authenticate.";
31049
31050
  var TRANSIENT_HINT = "\n\nThis error is often transient. Retry the same call before trying a different approach (do not fall back to smaller writes or row-by-row operations).";
31051
+ var GRID_LIMIT_HINT = "\n\nThe target range is outside the sheet's current grid. Add the missing rows or columns first with gog_sheets_insert (dimension: rows or cols), then retry the write.";
31052
+ function formatAccountList(raw) {
31053
+ try {
31054
+ const parsed = JSON.parse(raw);
31055
+ if (Array.isArray(parsed?.accounts)) {
31056
+ return parsed.accounts.map((a) => a?.email).filter(Boolean).join("\n");
31057
+ }
31058
+ } catch {
31059
+ }
31060
+ return raw.trim();
31061
+ }
31062
+ async function diagnose(err) {
31063
+ const errText = toError(err).content[0].text;
31064
+ const isAuthError = AUTH_ERROR_PATTERN.test(errText);
31065
+ const isTransientError = !isAuthError && TRANSIENT_ERROR_PATTERN.test(errText);
31066
+ const isGridLimitError = GRID_LIMIT_ERROR_PATTERN.test(errText);
31067
+ const hint = isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : isGridLimitError ? GRID_LIMIT_HINT : "";
31068
+ try {
31069
+ const accounts = formatAccountList(await run(["auth", "list"]));
31070
+ return toText(`${errText}
31071
+
31072
+ Configured accounts:
31073
+ ${accounts || "(none)"}${hint}`);
31074
+ } catch {
31075
+ return toText(`${errText}${hint}`);
31076
+ }
31077
+ }
31050
31078
  async function runOrDiagnose(args, options) {
31051
31079
  try {
31052
31080
  return toText(await run(args, options));
31053
31081
  } catch (err) {
31054
- const base = toError(err);
31055
- const errText = base.content[0].text;
31056
- const isAuthError = AUTH_ERROR_PATTERN.test(errText);
31057
- const isTransientError = !isAuthError && TRANSIENT_ERROR_PATTERN.test(errText);
31058
- const hint = isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : "";
31059
- try {
31060
- const accounts = await run(["auth", "list"]);
31061
- return toText(`${errText}
31062
-
31063
- Configured accounts:
31064
- ${accounts}${hint}`);
31065
- } catch {
31066
- return toText(`${errText}${hint}`);
31067
- }
31082
+ return diagnose(err);
31068
31083
  }
31069
31084
  }
31070
31085
 
@@ -31166,7 +31181,7 @@ function registerCalendarTools(server) {
31166
31181
  return runOrDiagnose(["calendar", "event", calendarId, eventId], { account });
31167
31182
  });
31168
31183
  server.registerTool("gog_calendar_create", {
31169
- description: "Create a calendar event.",
31184
+ description: "Create a calendar event. Set withZoom=true to attach a Zoom meeting (requires Zoom S2S OAuth setup via gog_zoom_auth_setup; the join URL + meeting ID + passcode are appended to the event description \u2014 Google rejects native conference card writes from non-Workspace-Marketplace OAuth clients).",
31170
31185
  annotations: { destructiveHint: false },
31171
31186
  inputSchema: {
31172
31187
  calendarId: external_exports.string().describe('Calendar ID (use "primary" for the default calendar)'),
@@ -31177,18 +31192,20 @@ function registerCalendarTools(server) {
31177
31192
  location: external_exports.string().optional().describe("Event location"),
31178
31193
  attendees: external_exports.string().optional().describe("Attendee emails, comma-separated"),
31179
31194
  allDay: external_exports.boolean().optional().describe("All-day event (use date-only in from/to)"),
31195
+ withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)"),
31180
31196
  account: accountParam
31181
31197
  }
31182
- }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, account }) => {
31198
+ }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, withZoom, account }) => {
31183
31199
  const args = ["calendar", "create", calendarId, `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
31184
31200
  if (description) args.push(`--description=${description}`);
31185
31201
  if (location) args.push(`--location=${location}`);
31186
31202
  if (attendees) args.push(`--attendees=${attendees}`);
31187
31203
  if (allDay) args.push("--all-day");
31204
+ if (withZoom) args.push("--with-zoom");
31188
31205
  return runOrDiagnose(args, { account });
31189
31206
  });
31190
31207
  server.registerTool("gog_calendar_update", {
31191
- description: "Update an existing calendar event.",
31208
+ description: "Update an existing calendar event. Zoom: withZoom adds a Zoom meeting, regenerateZoom replaces the existing one, removeZoom strips it (each are independent \u2014 use one per call).",
31192
31209
  annotations: { destructiveHint: false },
31193
31210
  inputSchema: {
31194
31211
  calendarId: external_exports.string().describe("Calendar ID"),
@@ -31199,9 +31216,12 @@ function registerCalendarTools(server) {
31199
31216
  description: external_exports.string().optional().describe("New description"),
31200
31217
  location: external_exports.string().optional().describe("New location"),
31201
31218
  attendees: external_exports.string().optional().describe("New attendee emails, comma-separated (replaces existing)"),
31219
+ withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event"),
31220
+ regenerateZoom: external_exports.boolean().optional().describe("Replace the event's existing Zoom video conference"),
31221
+ removeZoom: external_exports.boolean().optional().describe("Remove the event's Zoom video conference"),
31202
31222
  account: accountParam
31203
31223
  }
31204
- }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, account }) => {
31224
+ }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, withZoom, regenerateZoom, removeZoom, account }) => {
31205
31225
  const args = ["calendar", "update", calendarId, eventId];
31206
31226
  if (summary !== void 0) args.push(`--summary=${summary}`);
31207
31227
  if (from !== void 0) args.push(`--from=${from}`);
@@ -31209,6 +31229,9 @@ function registerCalendarTools(server) {
31209
31229
  if (description !== void 0) args.push(`--description=${description}`);
31210
31230
  if (location !== void 0) args.push(`--location=${location}`);
31211
31231
  if (attendees !== void 0) args.push(`--attendees=${attendees}`);
31232
+ if (withZoom) args.push("--with-zoom");
31233
+ if (regenerateZoom) args.push("--regenerate-zoom");
31234
+ if (removeZoom) args.push("--remove-zoom");
31212
31235
  return runOrDiagnose(args, { account });
31213
31236
  });
31214
31237
  server.registerTool("gog_calendar_delete", {
@@ -31845,7 +31868,7 @@ function registerDriveTools(server) {
31845
31868
  // src/tools/gmail.ts
31846
31869
  function registerGmailTools(server) {
31847
31870
  server.registerTool("gog_gmail_search", {
31848
- description: 'Search Gmail threads using Gmail query syntax (e.g. "from:alice subject:invoice is:unread").',
31871
+ description: `Search Gmail threads using Gmail query syntax (e.g. "from:alice subject:invoice is:unread"). The query is passed verbatim to Gmail; a bare name token (from:alison) matches per Gmail's own heuristics, a full address (from:alison@example.com) is exact. To match a contact across several addresses, OR them: from:(a@x.com OR b@y.com).`,
31849
31872
  annotations: { readOnlyHint: true },
31850
31873
  inputSchema: {
31851
31874
  query: external_exports.string().describe("Gmail search query"),
@@ -31894,8 +31917,62 @@ function registerGmailTools(server) {
31894
31917
  registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
31895
31918
  }
31896
31919
 
31920
+ // src/tools/sheets-a1.ts
31921
+ function colToLetter(n) {
31922
+ let s = "";
31923
+ while (n > 0) {
31924
+ const rem = (n - 1) % 26;
31925
+ s = String.fromCharCode(65 + rem) + s;
31926
+ n = Math.floor((n - 1) / 26);
31927
+ }
31928
+ return s;
31929
+ }
31930
+ function letterToCol(s) {
31931
+ let n = 0;
31932
+ for (const ch of s.toUpperCase()) {
31933
+ n = n * 26 + (ch.charCodeAt(0) - 64);
31934
+ }
31935
+ return n;
31936
+ }
31937
+ function expandAnchorRange(range, rows, cols) {
31938
+ const bang = range.lastIndexOf("!");
31939
+ const sheet = bang >= 0 ? range.slice(0, bang + 1) : "";
31940
+ const cell = bang >= 0 ? range.slice(bang + 1) : range;
31941
+ const m = /^([A-Za-z]+)([0-9]+)$/.exec(cell);
31942
+ if (!m) return range;
31943
+ const startCol = letterToCol(m[1]);
31944
+ const startRow = parseInt(m[2], 10);
31945
+ const endCol = colToLetter(startCol + cols - 1);
31946
+ const endRow = startRow + rows - 1;
31947
+ return `${sheet}${m[1].toUpperCase()}${startRow}:${endCol}${endRow}`;
31948
+ }
31949
+ function countNonEmptyCells(getOutput) {
31950
+ let parsed;
31951
+ try {
31952
+ parsed = JSON.parse(getOutput);
31953
+ } catch {
31954
+ return -1;
31955
+ }
31956
+ const values = parsed?.values;
31957
+ if (!Array.isArray(values)) return 0;
31958
+ let count = 0;
31959
+ for (const row of values) {
31960
+ if (!Array.isArray(row)) continue;
31961
+ for (const cell of row) {
31962
+ if (cell !== null && String(cell).trim() !== "") count++;
31963
+ }
31964
+ }
31965
+ return count;
31966
+ }
31967
+
31897
31968
  // src/tools/sheets.ts
31898
31969
  var cellValueParam = external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]);
31970
+ var dryRunParam = external_exports.boolean().optional().describe(
31971
+ "Preview the operation without modifying the sheet (gog --dry-run): reports the intended actions and exits without writing."
31972
+ );
31973
+ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
31974
+ 'Safety guard against silent overwrites: before writing, read the target range and refuse the write if any target cell already holds data. Costs one extra read. Anchor ranges (e.g. "Sheet1!A1") are expanded to the full area your values will cover; explicit and named ranges are checked as-is.'
31975
+ );
31899
31976
  function registerSheetsTools(server) {
31900
31977
  server.registerTool("gog_sheets_get", {
31901
31978
  description: 'Read values from a Google Sheets range. Returns a JSON object with a "values" array of rows.',
@@ -31915,13 +31992,31 @@ function registerSheetsTools(server) {
31915
31992
  spreadsheetId: external_exports.string().describe("Spreadsheet ID (from the URL)"),
31916
31993
  range: external_exports.string().describe("Top-left cell or range in A1 notation, e.g. Sheet1!A1"),
31917
31994
  values: external_exports.array(external_exports.array(cellValueParam)).describe('2D array of values (rows of columns). Cells may be string/number/boolean/null; strings starting with "=" are formulas.'),
31995
+ dry_run: dryRunParam,
31996
+ fail_if_not_empty: failIfNotEmptyParam,
31918
31997
  account: accountParam
31919
31998
  }
31920
- }, async ({ spreadsheetId, range, values, account }) => {
31921
- return runOrDiagnose(
31922
- ["sheets", "update", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`],
31923
- { account }
31924
- );
31999
+ }, async ({ spreadsheetId, range, values, account, dry_run, fail_if_not_empty }) => {
32000
+ const cols = values.reduce((max, row) => Math.max(max, row.length), 0);
32001
+ if (fail_if_not_empty && values.length > 0 && cols > 0) {
32002
+ const readRange = expandAnchorRange(range, values.length, cols);
32003
+ let existing;
32004
+ try {
32005
+ existing = await run(["sheets", "get", spreadsheetId, readRange], { account });
32006
+ } catch (err) {
32007
+ return diagnose(err);
32008
+ }
32009
+ const occupied = countNonEmptyCells(existing);
32010
+ if (occupied !== 0) {
32011
+ const detail = occupied < 0 ? "could not be verified as empty" : `already contains data in ${occupied} cell(s)`;
32012
+ return toText(
32013
+ `Write aborted (fail_if_not_empty): target range ${readRange} ${detail}. Re-run without fail_if_not_empty to overwrite, or clear it first with gog_sheets_clear.`
32014
+ );
32015
+ }
32016
+ }
32017
+ const args = ["sheets", "update", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`];
32018
+ if (dry_run) args.push("--dry-run");
32019
+ return runOrDiagnose(args, { account });
31925
32020
  });
31926
32021
  server.registerTool("gog_sheets_append", {
31927
32022
  description: 'Append rows to a Google Sheet after the last row with data in the given range. Values may be strings, numbers, booleans, or null. Strings starting with "=" are interpreted as formulas.',
@@ -31930,13 +32025,13 @@ function registerSheetsTools(server) {
31930
32025
  spreadsheetId: external_exports.string().describe("Spreadsheet ID (from the URL)"),
31931
32026
  range: external_exports.string().describe("Range indicating which sheet/columns to append to, e.g. Sheet1!A:C"),
31932
32027
  values: external_exports.array(external_exports.array(cellValueParam)).describe('2D array of rows to append. Cells may be string/number/boolean/null; strings starting with "=" are formulas.'),
32028
+ dry_run: dryRunParam,
31933
32029
  account: accountParam
31934
32030
  }
31935
- }, async ({ spreadsheetId, range, values, account }) => {
31936
- return runOrDiagnose(
31937
- ["sheets", "append", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`],
31938
- { account }
31939
- );
32031
+ }, async ({ spreadsheetId, range, values, account, dry_run }) => {
32032
+ const args = ["sheets", "append", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`];
32033
+ if (dry_run) args.push("--dry-run");
32034
+ return runOrDiagnose(args, { account });
31940
32035
  });
31941
32036
  server.registerTool("gog_sheets_clear", {
31942
32037
  description: "Clear all values in a Google Sheets range (formatting is preserved).",
@@ -31944,13 +32039,16 @@ function registerSheetsTools(server) {
31944
32039
  inputSchema: {
31945
32040
  spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31946
32041
  range: external_exports.string().describe("Range in A1 notation to clear"),
32042
+ dry_run: dryRunParam,
31947
32043
  account: accountParam
31948
32044
  }
31949
- }, async ({ spreadsheetId, range, account }) => {
31950
- return runOrDiagnose(["sheets", "clear", spreadsheetId, range], { account });
32045
+ }, async ({ spreadsheetId, range, account, dry_run }) => {
32046
+ const args = ["sheets", "clear", spreadsheetId, range];
32047
+ if (dry_run) args.push("--dry-run");
32048
+ return runOrDiagnose(args, { account });
31951
32049
  });
31952
32050
  server.registerTool("gog_sheets_metadata", {
31953
- description: "Get spreadsheet metadata: title, sheet tabs, named ranges, and other properties.",
32051
+ description: "Get spreadsheet metadata: title, named ranges, and per-tab properties including grid dimensions (gridProperties.rowCount / columnCount). Use this to learn a sheet's current size before writing \u2014 a write outside the grid fails.",
31954
32052
  annotations: { readOnlyHint: true },
31955
32053
  inputSchema: {
31956
32054
  spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
@@ -32136,7 +32234,7 @@ function registerTasksTools(server) {
32136
32234
  }
32137
32235
 
32138
32236
  // src/server.ts
32139
- var VERSION = true ? "2.0.11" : "0.0.0";
32237
+ var VERSION = true ? "2.2.0" : "0.0.0";
32140
32238
  function createServer(options) {
32141
32239
  return new McpServer({
32142
32240
  name: options?.name ?? "gogcli",
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.0.11",
6
+ "version": "2.2.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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp",
3
- "version": "2.0.11",
3
+ "version": "2.2.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>",
@@ -45,8 +45,8 @@
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "devDependencies": {
48
- "@types/node": "^25.6.2",
49
- "@vitest/coverage-v8": "^4.1.6",
48
+ "@types/node": "^25.9.1",
49
+ "@vitest/coverage-v8": "^4.1.7",
50
50
  "esbuild": "^0.28.0",
51
51
  "typescript": "^6.0.2",
52
52
  "vitest": "^4.1.6"