gogcli-mcp-sheets 2.0.12 → 2.3.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/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp-sheets"
9
9
  },
10
- "version": "2.0.12",
10
+ "version": "2.3.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-sheets",
15
- "version": "2.0.12",
15
+ "version": "2.3.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -18,6 +18,55 @@ export function hexToRgb(hex: string): { red: number; green: number; blue: numbe
18
18
  };
19
19
  }
20
20
 
21
+ // Issue #43: peek the target range before applying DATE / DATE_TIME number
22
+ // formatting. If every numeric cell is a small integer (< 10000), Sheets will
23
+ // render them as day-serials near 1899-12-30 — almost certainly not what the
24
+ // caller intended. Return a warning string in that case, otherwise null.
25
+ //
26
+ // Returns null (no warning) when:
27
+ // - the peek fails / response isn't parseable JSON
28
+ // - the range is empty
29
+ // - any value is a non-integer number or a number >= 10000
30
+ // - any value is a string (other than empty), boolean, etc.
31
+ // Nulls and empty strings are ignored.
32
+ async function checkDateFormatTarget(
33
+ spreadsheetId: string,
34
+ range: string,
35
+ account: string | undefined,
36
+ ): Promise<string | null> {
37
+ const peek = await runOrDiagnose(
38
+ ['sheets', 'get', spreadsheetId, range, '--render=UNFORMATTED_VALUE'],
39
+ { account },
40
+ );
41
+ let parsed: { values?: unknown[][] };
42
+ try {
43
+ parsed = JSON.parse(peek.content[0].text) as { values?: unknown[][] };
44
+ } catch {
45
+ return null;
46
+ }
47
+ const rows = parsed.values;
48
+ if (!Array.isArray(rows) || rows.length === 0) return null;
49
+ let sawSmallInt = false;
50
+ for (const row of rows) {
51
+ for (const cell of row) {
52
+ if (cell === null || cell === undefined || cell === '') continue;
53
+ if (typeof cell !== 'number') return null;
54
+ if (!Number.isInteger(cell)) return null;
55
+ if (cell < 0) return null;
56
+ if (cell >= 10000) return null;
57
+ sawSmallInt = true;
58
+ }
59
+ }
60
+ if (!sawSmallInt) return null;
61
+ return (
62
+ 'Warning: applying DATE/DATE_TIME format to cells holding small integers (< 10000) ' +
63
+ 'will render them as dates near 1899-12-30 because Sheets interprets numeric values as ' +
64
+ 'day-serials from that epoch. If those integers are ordinals (1, 2, 3, ...) and not ' +
65
+ 'day offsets, this is almost certainly not what you want. Pass force:true to suppress ' +
66
+ 'this warning, or convert the cells to real dates / strings first.'
67
+ );
68
+ }
69
+
21
70
  export function registerExtraSheetsTools(server: McpServer): void {
22
71
 
23
72
  server.registerTool('gog_sheets_list_tabs', {
@@ -120,7 +169,7 @@ export function registerExtraSheetsTools(server: McpServer): void {
120
169
  });
121
170
 
122
171
  server.registerTool('gog_sheets_insert', {
123
- description: 'Insert rows or columns into a sheet.',
172
+ description: 'Insert rows or columns into a sheet. With after:false (default), the new dimension lands at start. With after:true, the new dimension lands at start+1 (the existing dimension at start is preserved).',
124
173
  annotations: { destructiveHint: true },
125
174
  inputSchema: {
126
175
  spreadsheetId: z.string().describe('Spreadsheet ID'),
@@ -128,11 +177,16 @@ export function registerExtraSheetsTools(server: McpServer): void {
128
177
  dimension: z.string().describe('Dimension to insert: ROWS or COLUMNS'),
129
178
  start: z.number().describe('Start index (0-based)'),
130
179
  count: z.number().optional().describe('Number of rows/columns to insert (default: 1)'),
131
- after: z.boolean().optional().describe('Insert after the start index instead of before'),
180
+ after: z.boolean().optional().describe('Insert after the start index instead of before. With after:true the new dimension lands at start+1, leaving the existing dimension at start untouched.'),
132
181
  account: accountParam,
133
182
  },
134
183
  }, async ({ spreadsheetId, sheet, dimension, start, count, after, account }) => {
135
- const args = ['sheets', 'insert', spreadsheetId, sheet, dimension, String(start)];
184
+ // The underlying `gog sheets insert` treats `start` as 1-based with `--after` shifting +1.
185
+ // To honor our 0-based `start` and make `after:true` actually shift the insertion to start+1
186
+ // (issue #42), we send `start+1` when `after:true`. With after:false the CLI's `start-1`
187
+ // conversion lands the insertion at our 0-based start.
188
+ const effectiveStart = after ? start + 1 : start;
189
+ const args = ['sheets', 'insert', spreadsheetId, sheet, dimension, String(effectiveStart)];
136
190
  if (count !== undefined) args.push(`--count=${count}`);
137
191
  if (after) args.push('--after');
138
192
  return runOrDiagnose(args, { account });
@@ -255,20 +309,29 @@ export function registerExtraSheetsTools(server: McpServer): void {
255
309
  });
256
310
 
257
311
  server.registerTool('gog_sheets_number_format', {
258
- description: 'Set number format on a range (currency, percentage, date, etc.).',
312
+ description: 'Set number format on a range (currency, percentage, date, etc.). When type is DATE or DATE_TIME, the target range is peeked first; if every numeric cell is a small integer (< 10000), a warning is prepended to the response because Sheets will render those as 1899/1900 day-serials. Pass force:true to skip the check.',
259
313
  annotations: { destructiveHint: true },
260
314
  inputSchema: {
261
315
  spreadsheetId: z.string().describe('Spreadsheet ID'),
262
316
  range: z.string().describe('Range to format (e.g. Sheet1!A1:A10)'),
263
- type: z.string().optional().describe('Format type: NUMBER, CURRENCY, PERCENT, DATE, TIME, SCIENTIFIC, etc.'),
317
+ type: z.string().optional().describe('Format type: NUMBER, CURRENCY, PERCENT, DATE, DATE_TIME, TIME, SCIENTIFIC, etc.'),
264
318
  pattern: z.string().optional().describe('Custom format pattern (e.g. "#,##0.00", "yyyy-mm-dd")'),
319
+ force: z.boolean().optional().describe('Skip the DATE/DATE_TIME small-integer warning check'),
265
320
  account: accountParam,
266
321
  },
267
- }, async ({ spreadsheetId, range, type, pattern, account }) => {
322
+ }, async ({ spreadsheetId, range, type, pattern, force, account }) => {
323
+ const isDateType = type === 'DATE' || type === 'DATE_TIME';
324
+ const warning = isDateType && !force
325
+ ? await checkDateFormatTarget(spreadsheetId, range, account)
326
+ : null;
268
327
  const args = ['sheets', 'number-format', spreadsheetId, range];
269
328
  if (type) args.push(`--type=${type}`);
270
329
  if (pattern) args.push(`--pattern=${pattern}`);
271
- return runOrDiagnose(args, { account });
330
+ const result = await runOrDiagnose(args, { account });
331
+ if (warning) {
332
+ return { content: [{ type: 'text' as const, text: `${warning}\n\n${result.content[0].text}` }] };
333
+ }
334
+ return result;
272
335
  });
273
336
 
274
337
  server.registerTool('gog_sheets_read_format', {
@@ -457,4 +520,262 @@ export function registerExtraSheetsTools(server: McpServer): void {
457
520
  { account },
458
521
  );
459
522
  });
523
+
524
+ // ---- Charts (gog 0.19.0) ----
525
+
526
+ server.registerTool('gog_sheets_chart_list', {
527
+ description: 'List embedded charts in a spreadsheet (chartId, type, position).',
528
+ annotations: { readOnlyHint: true },
529
+ inputSchema: {
530
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
531
+ account: accountParam,
532
+ },
533
+ }, async ({ spreadsheetId, account }) => {
534
+ return runOrDiagnose(['sheets', 'chart', 'list', spreadsheetId], { account });
535
+ });
536
+
537
+ server.registerTool('gog_sheets_chart_get', {
538
+ description: 'Get the full definition (spec + position) of a single chart by its numeric chart ID.',
539
+ annotations: { readOnlyHint: true },
540
+ inputSchema: {
541
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
542
+ chartId: z.string().describe('Numeric chart ID (from gog_sheets_chart_list)'),
543
+ account: accountParam,
544
+ },
545
+ }, async ({ spreadsheetId, chartId, account }) => {
546
+ return runOrDiagnose(['sheets', 'chart', 'get', spreadsheetId, chartId], { account });
547
+ });
548
+
549
+ server.registerTool('gog_sheets_chart_create', {
550
+ description: 'Create an embedded chart from a JSON spec. specJson is a Sheets API ChartSpec (or full EmbeddedChart) — inline or @/path/to/file.json. Anchor the chart with sheet + anchor (A1 cell), and optionally size it with width/height pixels.',
551
+ inputSchema: {
552
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
553
+ specJson: z.string().describe('ChartSpec or EmbeddedChart JSON (inline or @file)'),
554
+ sheet: z.string().optional().describe('Sheet name for the anchor (resolved to sheetId)'),
555
+ anchor: z.string().optional().describe('Anchor cell in A1 notation (e.g. A1, E10)'),
556
+ width: z.number().optional().describe('Chart width in pixels (default: 600)'),
557
+ height: z.number().optional().describe('Chart height in pixels (default: 371)'),
558
+ account: accountParam,
559
+ },
560
+ }, async ({ spreadsheetId, specJson, sheet, anchor, width, height, account }) => {
561
+ const args = ['sheets', 'chart', 'create', spreadsheetId, `--spec-json=${specJson}`];
562
+ if (sheet) args.push(`--sheet=${sheet}`);
563
+ if (anchor) args.push(`--anchor=${anchor}`);
564
+ if (width !== undefined) args.push(`--width=${width}`);
565
+ if (height !== undefined) args.push(`--height=${height}`);
566
+ return runOrDiagnose(args, { account });
567
+ });
568
+
569
+ server.registerTool('gog_sheets_chart_update', {
570
+ description: 'Replace a chart spec by chart ID. specJson is a Sheets API ChartSpec (or full EmbeddedChart) — inline or @/path/to/file.json.',
571
+ annotations: { destructiveHint: true },
572
+ inputSchema: {
573
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
574
+ chartId: z.string().describe('Numeric chart ID to update'),
575
+ specJson: z.string().describe('ChartSpec or EmbeddedChart JSON (inline or @file)'),
576
+ account: accountParam,
577
+ },
578
+ }, async ({ spreadsheetId, chartId, specJson, account }) => {
579
+ return runOrDiagnose(
580
+ ['sheets', 'chart', 'update', spreadsheetId, chartId, `--spec-json=${specJson}`],
581
+ { account },
582
+ );
583
+ });
584
+
585
+ server.registerTool('gog_sheets_chart_delete', {
586
+ description: 'Delete a chart by its numeric chart ID.',
587
+ annotations: { destructiveHint: true },
588
+ inputSchema: {
589
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
590
+ chartId: z.string().describe('Numeric chart ID to delete'),
591
+ account: accountParam,
592
+ },
593
+ }, async ({ spreadsheetId, chartId, account }) => {
594
+ return runOrDiagnose(['sheets', 'chart', 'delete', spreadsheetId, chartId], { account });
595
+ });
596
+
597
+ // ---- Tables (gog 0.19.0) ----
598
+
599
+ server.registerTool('gog_sheets_table_list', {
600
+ description: 'List Google Sheets tables in a spreadsheet (tableId, name, range).',
601
+ annotations: { readOnlyHint: true },
602
+ inputSchema: {
603
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
604
+ account: accountParam,
605
+ },
606
+ }, async ({ spreadsheetId, account }) => {
607
+ return runOrDiagnose(['sheets', 'table', 'list', spreadsheetId], { account });
608
+ });
609
+
610
+ server.registerTool('gog_sheets_table_get', {
611
+ description: 'Get a single Google Sheets table (definition + columns) by its table ID.',
612
+ annotations: { readOnlyHint: true },
613
+ inputSchema: {
614
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
615
+ tableId: z.string().describe('Table ID (from gog_sheets_table_list)'),
616
+ account: accountParam,
617
+ },
618
+ }, async ({ spreadsheetId, tableId, account }) => {
619
+ return runOrDiagnose(['sheets', 'table', 'get', spreadsheetId, tableId], { account });
620
+ });
621
+
622
+ server.registerTool('gog_sheets_table_create', {
623
+ description: 'Create a Google Sheets table over a range. columnsJson is a JSON array of column definitions (each {columnName, columnType?}); valid columnType values: TEXT, DOUBLE, BOOLEAN, DATE, DROPDOWN. Inline JSON or @/path/to/file.json.',
624
+ inputSchema: {
625
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
626
+ range: z.string().describe('Range the table covers (e.g. Sheet1!A1:D20)'),
627
+ name: z.string().describe('Table name'),
628
+ columnsJson: z.string().describe('Column definitions as JSON array or @file (columnName + optional columnType)'),
629
+ account: accountParam,
630
+ },
631
+ }, async ({ spreadsheetId, range, name, columnsJson, account }) => {
632
+ return runOrDiagnose(
633
+ ['sheets', 'table', 'create', spreadsheetId, range, `--name=${name}`, `--columns-json=${columnsJson}`],
634
+ { account },
635
+ );
636
+ });
637
+
638
+ server.registerTool('gog_sheets_table_append', {
639
+ description: 'Append data rows to a table. valuesJson is a JSON 2D array of row values.',
640
+ inputSchema: {
641
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
642
+ tableId: z.string().describe('Table ID to append to'),
643
+ valuesJson: z.string().describe('Values as JSON 2D array (e.g. [["a",1],["b",2]])'),
644
+ input: z.enum(['RAW', 'USER_ENTERED']).optional().describe('Value input option (default: USER_ENTERED)'),
645
+ account: accountParam,
646
+ },
647
+ }, async ({ spreadsheetId, tableId, valuesJson, input, account }) => {
648
+ const args = ['sheets', 'table', 'append', spreadsheetId, tableId, `--values-json=${valuesJson}`];
649
+ if (input) args.push(`--input=${input}`);
650
+ return runOrDiagnose(args, { account });
651
+ });
652
+
653
+ server.registerTool('gog_sheets_table_clear', {
654
+ description: 'Clear all data rows from a table (keeps the table and its columns).',
655
+ annotations: { destructiveHint: true },
656
+ inputSchema: {
657
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
658
+ tableId: z.string().describe('Table ID to clear'),
659
+ account: accountParam,
660
+ },
661
+ }, async ({ spreadsheetId, tableId, account }) => {
662
+ return runOrDiagnose(['sheets', 'table', 'clear', spreadsheetId, tableId], { account });
663
+ });
664
+
665
+ server.registerTool('gog_sheets_table_delete', {
666
+ description: 'Delete a table by its table ID.',
667
+ annotations: { destructiveHint: true },
668
+ inputSchema: {
669
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
670
+ tableId: z.string().describe('Table ID to delete'),
671
+ account: accountParam,
672
+ },
673
+ }, async ({ spreadsheetId, tableId, account }) => {
674
+ return runOrDiagnose(['sheets', 'table', 'delete', spreadsheetId, tableId], { account });
675
+ });
676
+
677
+ // ---- Banding / alternating colors (gog 0.19.0) ----
678
+
679
+ server.registerTool('gog_sheets_banding_list', {
680
+ description: 'List alternating-color banded ranges. Optionally scope to a single sheet.',
681
+ annotations: { readOnlyHint: true },
682
+ inputSchema: {
683
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
684
+ sheet: z.string().optional().describe('Only list banding from this sheet'),
685
+ account: accountParam,
686
+ },
687
+ }, async ({ spreadsheetId, sheet, account }) => {
688
+ const args = ['sheets', 'banding', 'list', spreadsheetId];
689
+ if (sheet) args.push(`--sheet=${sheet}`);
690
+ return runOrDiagnose(args, { account });
691
+ });
692
+
693
+ server.registerTool('gog_sheets_banding_set', {
694
+ description: 'Apply alternating colors to a range. Provide rowPropertiesJson and/or columnPropertiesJson — each a Sheets API BandingProperties JSON object ({headerColor, firstBandColor, secondBandColor, footerColor}). At least one is required.',
695
+ inputSchema: {
696
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
697
+ range: z.string().describe('Range to band (e.g. Sheet1!A1:D20)'),
698
+ rowPropertiesJson: z.string().optional().describe('BandingProperties JSON for row colors'),
699
+ columnPropertiesJson: z.string().optional().describe('BandingProperties JSON for column colors'),
700
+ account: accountParam,
701
+ },
702
+ }, async ({ spreadsheetId, range, rowPropertiesJson, columnPropertiesJson, account }) => {
703
+ const args = ['sheets', 'banding', 'set', spreadsheetId, range];
704
+ if (rowPropertiesJson) args.push(`--row-properties-json=${rowPropertiesJson}`);
705
+ if (columnPropertiesJson) args.push(`--column-properties-json=${columnPropertiesJson}`);
706
+ return runOrDiagnose(args, { account });
707
+ });
708
+
709
+ server.registerTool('gog_sheets_banding_clear', {
710
+ description: 'Remove alternating-color banding. Pass id to remove a single banded range, or all:true with sheet to remove every banding on that sheet.',
711
+ annotations: { destructiveHint: true },
712
+ inputSchema: {
713
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
714
+ id: z.number().optional().describe('Banded range ID to remove'),
715
+ all: z.boolean().optional().describe('Remove all banding from the sheet (requires sheet)'),
716
+ sheet: z.string().optional().describe('Sheet name (used with all:true)'),
717
+ account: accountParam,
718
+ },
719
+ }, async ({ spreadsheetId, id, all, sheet, account }) => {
720
+ const args = ['sheets', 'banding', 'clear', spreadsheetId];
721
+ if (id !== undefined) args.push(`--id=${id}`);
722
+ if (all) args.push('--all');
723
+ if (sheet) args.push(`--sheet=${sheet}`);
724
+ return runOrDiagnose(args, { account });
725
+ });
726
+
727
+ // ---- Conditional formatting (gog 0.19.0) ----
728
+
729
+ server.registerTool('gog_sheets_conditional_format_list', {
730
+ description: 'List conditional formatting rules. Optionally scope to a single sheet.',
731
+ annotations: { readOnlyHint: true },
732
+ inputSchema: {
733
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
734
+ sheet: z.string().optional().describe('Only list rules from this sheet'),
735
+ account: accountParam,
736
+ },
737
+ }, async ({ spreadsheetId, sheet, account }) => {
738
+ const args = ['sheets', 'conditional-format', 'list', spreadsheetId];
739
+ if (sheet) args.push(`--sheet=${sheet}`);
740
+ return runOrDiagnose(args, { account });
741
+ });
742
+
743
+ server.registerTool('gog_sheets_conditional_format_add', {
744
+ description: 'Add a conditional formatting rule to a range. type picks the condition; expr is its value/formula (omit for blank/not-blank). formatJson is the CellFormat to apply when the condition matches (inline or @file). Use formatFields to force-send zero/false fields (e.g. backgroundColor,textFormat.bold).',
745
+ inputSchema: {
746
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
747
+ range: z.string().describe('Range the rule applies to (e.g. Sheet1!A1:A100)'),
748
+ type: z.enum([
749
+ 'text-eq', 'text-contains', 'text-starts-with', 'text-ends-with',
750
+ 'number-eq', 'number-gt', 'number-gte', 'number-lt', 'number-lte',
751
+ 'blank', 'not-blank', 'custom-formula',
752
+ ]).describe('Rule type'),
753
+ formatJson: z.string().describe('CellFormat JSON to apply when the condition matches (inline or @file)'),
754
+ expr: z.string().optional().describe('Expression value or custom formula (omit for blank/not-blank)'),
755
+ formatFields: z.string().optional().describe('Format field mask for force-sending zero/false fields (e.g. backgroundColor,textFormat.bold)'),
756
+ account: accountParam,
757
+ },
758
+ }, async ({ spreadsheetId, range, type, formatJson, expr, formatFields, account }) => {
759
+ const args = ['sheets', 'conditional-format', 'add', spreadsheetId, range, `--type=${type}`, `--format-json=${formatJson}`];
760
+ if (expr !== undefined) args.push(`--expr=${expr}`);
761
+ if (formatFields) args.push(`--format-fields=${formatFields}`);
762
+ return runOrDiagnose(args, { account });
763
+ });
764
+
765
+ server.registerTool('gog_sheets_conditional_format_clear', {
766
+ description: 'Remove conditional formatting rules from a sheet. Pass index to remove a single rule by its 0-based index, or all:true to remove every rule on the sheet.',
767
+ annotations: { destructiveHint: true },
768
+ inputSchema: {
769
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
770
+ sheet: z.string().describe('Sheet name to clear rules from'),
771
+ index: z.number().optional().describe('0-based rule index to remove'),
772
+ all: z.boolean().optional().describe('Remove all conditional formatting rules from the sheet'),
773
+ account: accountParam,
774
+ },
775
+ }, async ({ spreadsheetId, sheet, index, all, account }) => {
776
+ const args = ['sheets', 'conditional-format', 'clear', spreadsheetId, `--sheet=${sheet}`];
777
+ if (index !== undefined) args.push(`--index=${index}`);
778
+ if (all) args.push('--all');
779
+ return runOrDiagnose(args, { account });
780
+ });
460
781
  }