gogcli-mcp-sheets 2.2.0 → 2.4.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": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
10
- "version": "2.2.0"
10
+ "version": "2.4.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "gogcli (Sheets)",
16
16
  "source": "./",
17
17
  "description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
18
- "version": "2.2.0",
18
+ "version": "2.4.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp-sheets",
3
3
  "displayName": "gogcli (Sheets)",
4
- "version": "2.2.0",
4
+ "version": "2.4.0",
5
5
  "description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/index.js CHANGED
@@ -31399,7 +31399,7 @@ function registerSheetsTools(server2) {
31399
31399
  }
31400
31400
 
31401
31401
  // ../gogcli-mcp/src/server.ts
31402
- var VERSION = true ? "2.2.0" : "0.0.0";
31402
+ var VERSION = true ? "2.4.0" : "0.0.0";
31403
31403
  function createServer(options) {
31404
31404
  return new McpServer({
31405
31405
  name: options?.name ?? "gogcli",
@@ -31548,13 +31548,32 @@ function registerExtraSheetsTools(server2) {
31548
31548
  start: external_exports.number().describe("Start index (0-based)"),
31549
31549
  count: external_exports.number().optional().describe("Number of rows/columns to insert (default: 1)"),
31550
31550
  after: external_exports.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."),
31551
+ inheritFromBefore: external_exports.boolean().optional().describe("Inherit number format / styling from the row/column before the insertion. Defaults to true with after:true, false otherwise; pass false to inherit from the row/column after the insertion instead. Cannot inherit from before when inserting at the first row/column."),
31551
31552
  account: accountParam
31552
31553
  }
31553
- }, async ({ spreadsheetId, sheet, dimension, start, count, after, account }) => {
31554
+ }, async ({ spreadsheetId, sheet, dimension, start, count, after, inheritFromBefore, account }) => {
31554
31555
  const effectiveStart = after ? start + 1 : start;
31555
31556
  const args = ["sheets", "insert", spreadsheetId, sheet, dimension, String(effectiveStart)];
31556
31557
  if (count !== void 0) args.push(`--count=${count}`);
31557
31558
  if (after) args.push("--after");
31559
+ if (inheritFromBefore !== void 0) args.push(`--inherit-from-before=${inheritFromBefore}`);
31560
+ return runOrDiagnose(args, { account });
31561
+ });
31562
+ server2.registerTool("gog_sheets_copy_paste", {
31563
+ description: "Copy a source range's values/formulas/format to a destination range via the Sheets CopyPasteRequest. A destination larger than the source tiles the source to fill it \u2014 the canonical way to fill formulas down or across with relative references adjusted (aliases: fill, copy-range). Use type to control what is pasted (NORMAL pastes everything; FORMULA fills formulas with adjusted references; VALUES/FORMAT/etc. paste only that aspect).",
31564
+ annotations: { destructiveHint: true },
31565
+ inputSchema: {
31566
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31567
+ source: external_exports.string().describe("Source range (e.g. Sheet1!A2:H71)"),
31568
+ dest: external_exports.string().describe("Destination range (e.g. Sheet1!A2:H120). Larger than the source tiles/fills it."),
31569
+ type: external_exports.enum(["NORMAL", "VALUES", "FORMAT", "FORMULA", "NO_BORDERS", "DATA_VALIDATION", "CONDITIONAL_FORMATTING"]).optional().describe("Paste type (default: NORMAL pastes everything)"),
31570
+ transpose: external_exports.boolean().optional().describe("Paste transposed (swap rows and columns)"),
31571
+ account: accountParam
31572
+ }
31573
+ }, async ({ spreadsheetId, source, dest, type, transpose, account }) => {
31574
+ const args = ["sheets", "copy-paste", spreadsheetId, source, dest];
31575
+ if (type) args.push(`--type=${type}`);
31576
+ if (transpose) args.push("--transpose");
31558
31577
  return runOrDiagnose(args, { account });
31559
31578
  });
31560
31579
  server2.registerTool("gog_sheets_merge", {
@@ -31835,11 +31854,11 @@ ${result.content[0].text}` }] };
31835
31854
  return runOrDiagnose(["sheets", "named-ranges", "delete", spreadsheetId, nameOrId], { account });
31836
31855
  });
31837
31856
  server2.registerTool("gog_sheets_batch_update", {
31838
- description: 'Update values in multiple ranges with one Sheets API request. dataJson is a JSON array of {range, values} objects (or "@/path/to/file.json"). Atomic \u2014 either all ranges update or none do.',
31857
+ description: `Update values in multiple ranges with one Sheets API request. dataJson is a JSON array of {range, values} objects \u2014 pass it inline as a literal JSON string. Atomic \u2014 either all ranges update or none do. (The CLI also accepts "@/path/to/file.json", but that file is read on the gog server's filesystem, not the caller's, so inline JSON is the right form for remote MCP callers.)`,
31839
31858
  annotations: { destructiveHint: true },
31840
31859
  inputSchema: {
31841
31860
  spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31842
- dataJson: external_exports.string().describe('Value ranges as JSON array, or @file (e.g. [{"range":"Sheet1!A1:B2","values":[["a","b"]]}])'),
31861
+ dataJson: external_exports.string().describe('Value ranges as an inline JSON array, e.g. [{"range":"Sheet1!A1:B2","values":[["a","b"]]}]. (An "@/path" form is read on the gog server filesystem, not yours \u2014 inline the JSON instead.)'),
31843
31862
  input: external_exports.enum(["RAW", "USER_ENTERED"]).optional().describe("Value input option (default: USER_ENTERED)"),
31844
31863
  includeValuesInResponse: external_exports.boolean().optional().describe("Include updated values in the response"),
31845
31864
  responseRender: external_exports.enum(["FORMATTED_VALUE", "UNFORMATTED_VALUE", "FORMULA"]).optional().describe("Response value render option"),
@@ -31869,6 +31888,248 @@ ${result.content[0].text}` }] };
31869
31888
  { account }
31870
31889
  );
31871
31890
  });
31891
+ server2.registerTool("gog_sheets_chart_list", {
31892
+ description: "List embedded charts in a spreadsheet (chartId, type, position).",
31893
+ annotations: { readOnlyHint: true },
31894
+ inputSchema: {
31895
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31896
+ account: accountParam
31897
+ }
31898
+ }, async ({ spreadsheetId, account }) => {
31899
+ return runOrDiagnose(["sheets", "chart", "list", spreadsheetId], { account });
31900
+ });
31901
+ server2.registerTool("gog_sheets_chart_get", {
31902
+ description: "Get the full definition (spec + position) of a single chart by its numeric chart ID.",
31903
+ annotations: { readOnlyHint: true },
31904
+ inputSchema: {
31905
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31906
+ chartId: external_exports.string().describe("Numeric chart ID (from gog_sheets_chart_list)"),
31907
+ account: accountParam
31908
+ }
31909
+ }, async ({ spreadsheetId, chartId, account }) => {
31910
+ return runOrDiagnose(["sheets", "chart", "get", spreadsheetId, chartId], { account });
31911
+ });
31912
+ server2.registerTool("gog_sheets_chart_create", {
31913
+ description: "Create an embedded chart from a JSON spec. specJson is a Sheets API ChartSpec (or full EmbeddedChart) \u2014 inline or @/path/to/file.json. Anchor the chart with sheet + anchor (A1 cell), and optionally size it with width/height pixels.",
31914
+ inputSchema: {
31915
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31916
+ specJson: external_exports.string().describe("ChartSpec or EmbeddedChart JSON (inline or @file)"),
31917
+ sheet: external_exports.string().optional().describe("Sheet name for the anchor (resolved to sheetId)"),
31918
+ anchor: external_exports.string().optional().describe("Anchor cell in A1 notation (e.g. A1, E10)"),
31919
+ width: external_exports.number().optional().describe("Chart width in pixels (default: 600)"),
31920
+ height: external_exports.number().optional().describe("Chart height in pixels (default: 371)"),
31921
+ account: accountParam
31922
+ }
31923
+ }, async ({ spreadsheetId, specJson, sheet, anchor, width, height, account }) => {
31924
+ const args = ["sheets", "chart", "create", spreadsheetId, `--spec-json=${specJson}`];
31925
+ if (sheet) args.push(`--sheet=${sheet}`);
31926
+ if (anchor) args.push(`--anchor=${anchor}`);
31927
+ if (width !== void 0) args.push(`--width=${width}`);
31928
+ if (height !== void 0) args.push(`--height=${height}`);
31929
+ return runOrDiagnose(args, { account });
31930
+ });
31931
+ server2.registerTool("gog_sheets_chart_update", {
31932
+ description: "Replace a chart spec by chart ID. specJson is a Sheets API ChartSpec (or full EmbeddedChart) \u2014 inline or @/path/to/file.json.",
31933
+ annotations: { destructiveHint: true },
31934
+ inputSchema: {
31935
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31936
+ chartId: external_exports.string().describe("Numeric chart ID to update"),
31937
+ specJson: external_exports.string().describe("ChartSpec or EmbeddedChart JSON (inline or @file)"),
31938
+ account: accountParam
31939
+ }
31940
+ }, async ({ spreadsheetId, chartId, specJson, account }) => {
31941
+ return runOrDiagnose(
31942
+ ["sheets", "chart", "update", spreadsheetId, chartId, `--spec-json=${specJson}`],
31943
+ { account }
31944
+ );
31945
+ });
31946
+ server2.registerTool("gog_sheets_chart_delete", {
31947
+ description: "Delete a chart by its numeric chart ID.",
31948
+ annotations: { destructiveHint: true },
31949
+ inputSchema: {
31950
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31951
+ chartId: external_exports.string().describe("Numeric chart ID to delete"),
31952
+ account: accountParam
31953
+ }
31954
+ }, async ({ spreadsheetId, chartId, account }) => {
31955
+ return runOrDiagnose(["sheets", "chart", "delete", spreadsheetId, chartId], { account });
31956
+ });
31957
+ server2.registerTool("gog_sheets_table_list", {
31958
+ description: "List Google Sheets tables in a spreadsheet (tableId, name, range).",
31959
+ annotations: { readOnlyHint: true },
31960
+ inputSchema: {
31961
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31962
+ account: accountParam
31963
+ }
31964
+ }, async ({ spreadsheetId, account }) => {
31965
+ return runOrDiagnose(["sheets", "table", "list", spreadsheetId], { account });
31966
+ });
31967
+ server2.registerTool("gog_sheets_table_get", {
31968
+ description: "Get a single Google Sheets table (definition + columns) by its table ID.",
31969
+ annotations: { readOnlyHint: true },
31970
+ inputSchema: {
31971
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31972
+ tableId: external_exports.string().describe("Table ID (from gog_sheets_table_list)"),
31973
+ account: accountParam
31974
+ }
31975
+ }, async ({ spreadsheetId, tableId, account }) => {
31976
+ return runOrDiagnose(["sheets", "table", "get", spreadsheetId, tableId], { account });
31977
+ });
31978
+ server2.registerTool("gog_sheets_table_create", {
31979
+ 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.",
31980
+ inputSchema: {
31981
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31982
+ range: external_exports.string().describe("Range the table covers (e.g. Sheet1!A1:D20)"),
31983
+ name: external_exports.string().describe("Table name"),
31984
+ columnsJson: external_exports.string().describe("Column definitions as JSON array or @file (columnName + optional columnType)"),
31985
+ account: accountParam
31986
+ }
31987
+ }, async ({ spreadsheetId, range, name, columnsJson, account }) => {
31988
+ return runOrDiagnose(
31989
+ ["sheets", "table", "create", spreadsheetId, range, `--name=${name}`, `--columns-json=${columnsJson}`],
31990
+ { account }
31991
+ );
31992
+ });
31993
+ server2.registerTool("gog_sheets_table_append", {
31994
+ description: "Append data rows to a table. valuesJson is a JSON 2D array of row values.",
31995
+ inputSchema: {
31996
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31997
+ tableId: external_exports.string().describe("Table ID to append to"),
31998
+ valuesJson: external_exports.string().describe('Values as JSON 2D array (e.g. [["a",1],["b",2]])'),
31999
+ input: external_exports.enum(["RAW", "USER_ENTERED"]).optional().describe("Value input option (default: USER_ENTERED)"),
32000
+ account: accountParam
32001
+ }
32002
+ }, async ({ spreadsheetId, tableId, valuesJson, input, account }) => {
32003
+ const args = ["sheets", "table", "append", spreadsheetId, tableId, `--values-json=${valuesJson}`];
32004
+ if (input) args.push(`--input=${input}`);
32005
+ return runOrDiagnose(args, { account });
32006
+ });
32007
+ server2.registerTool("gog_sheets_table_clear", {
32008
+ description: "Clear all data rows from a table (keeps the table and its columns).",
32009
+ annotations: { destructiveHint: true },
32010
+ inputSchema: {
32011
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
32012
+ tableId: external_exports.string().describe("Table ID to clear"),
32013
+ account: accountParam
32014
+ }
32015
+ }, async ({ spreadsheetId, tableId, account }) => {
32016
+ return runOrDiagnose(["sheets", "table", "clear", spreadsheetId, tableId], { account });
32017
+ });
32018
+ server2.registerTool("gog_sheets_table_delete", {
32019
+ description: "Delete a table by its table ID.",
32020
+ annotations: { destructiveHint: true },
32021
+ inputSchema: {
32022
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
32023
+ tableId: external_exports.string().describe("Table ID to delete"),
32024
+ account: accountParam
32025
+ }
32026
+ }, async ({ spreadsheetId, tableId, account }) => {
32027
+ return runOrDiagnose(["sheets", "table", "delete", spreadsheetId, tableId], { account });
32028
+ });
32029
+ server2.registerTool("gog_sheets_banding_list", {
32030
+ description: "List alternating-color banded ranges. Optionally scope to a single sheet.",
32031
+ annotations: { readOnlyHint: true },
32032
+ inputSchema: {
32033
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
32034
+ sheet: external_exports.string().optional().describe("Only list banding from this sheet"),
32035
+ account: accountParam
32036
+ }
32037
+ }, async ({ spreadsheetId, sheet, account }) => {
32038
+ const args = ["sheets", "banding", "list", spreadsheetId];
32039
+ if (sheet) args.push(`--sheet=${sheet}`);
32040
+ return runOrDiagnose(args, { account });
32041
+ });
32042
+ server2.registerTool("gog_sheets_banding_set", {
32043
+ description: "Apply alternating colors to a range. Provide rowPropertiesJson and/or columnPropertiesJson \u2014 each a Sheets API BandingProperties JSON object ({headerColor, firstBandColor, secondBandColor, footerColor}). At least one is required.",
32044
+ inputSchema: {
32045
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
32046
+ range: external_exports.string().describe("Range to band (e.g. Sheet1!A1:D20)"),
32047
+ rowPropertiesJson: external_exports.string().optional().describe("BandingProperties JSON for row colors"),
32048
+ columnPropertiesJson: external_exports.string().optional().describe("BandingProperties JSON for column colors"),
32049
+ account: accountParam
32050
+ }
32051
+ }, async ({ spreadsheetId, range, rowPropertiesJson, columnPropertiesJson, account }) => {
32052
+ const args = ["sheets", "banding", "set", spreadsheetId, range];
32053
+ if (rowPropertiesJson) args.push(`--row-properties-json=${rowPropertiesJson}`);
32054
+ if (columnPropertiesJson) args.push(`--column-properties-json=${columnPropertiesJson}`);
32055
+ return runOrDiagnose(args, { account });
32056
+ });
32057
+ server2.registerTool("gog_sheets_banding_clear", {
32058
+ 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.",
32059
+ annotations: { destructiveHint: true },
32060
+ inputSchema: {
32061
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
32062
+ id: external_exports.number().optional().describe("Banded range ID to remove"),
32063
+ all: external_exports.boolean().optional().describe("Remove all banding from the sheet (requires sheet)"),
32064
+ sheet: external_exports.string().optional().describe("Sheet name (used with all:true)"),
32065
+ account: accountParam
32066
+ }
32067
+ }, async ({ spreadsheetId, id, all, sheet, account }) => {
32068
+ const args = ["sheets", "banding", "clear", spreadsheetId];
32069
+ if (id !== void 0) args.push(`--id=${id}`);
32070
+ if (all) args.push("--all");
32071
+ if (sheet) args.push(`--sheet=${sheet}`);
32072
+ return runOrDiagnose(args, { account });
32073
+ });
32074
+ server2.registerTool("gog_sheets_conditional_format_list", {
32075
+ description: "List conditional formatting rules. Optionally scope to a single sheet.",
32076
+ annotations: { readOnlyHint: true },
32077
+ inputSchema: {
32078
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
32079
+ sheet: external_exports.string().optional().describe("Only list rules from this sheet"),
32080
+ account: accountParam
32081
+ }
32082
+ }, async ({ spreadsheetId, sheet, account }) => {
32083
+ const args = ["sheets", "conditional-format", "list", spreadsheetId];
32084
+ if (sheet) args.push(`--sheet=${sheet}`);
32085
+ return runOrDiagnose(args, { account });
32086
+ });
32087
+ server2.registerTool("gog_sheets_conditional_format_add", {
32088
+ 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).",
32089
+ inputSchema: {
32090
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
32091
+ range: external_exports.string().describe("Range the rule applies to (e.g. Sheet1!A1:A100)"),
32092
+ type: external_exports.enum([
32093
+ "text-eq",
32094
+ "text-contains",
32095
+ "text-starts-with",
32096
+ "text-ends-with",
32097
+ "number-eq",
32098
+ "number-gt",
32099
+ "number-gte",
32100
+ "number-lt",
32101
+ "number-lte",
32102
+ "blank",
32103
+ "not-blank",
32104
+ "custom-formula"
32105
+ ]).describe("Rule type"),
32106
+ formatJson: external_exports.string().describe("CellFormat JSON to apply when the condition matches (inline or @file)"),
32107
+ expr: external_exports.string().optional().describe("Expression value or custom formula (omit for blank/not-blank)"),
32108
+ formatFields: external_exports.string().optional().describe("Format field mask for force-sending zero/false fields (e.g. backgroundColor,textFormat.bold)"),
32109
+ account: accountParam
32110
+ }
32111
+ }, async ({ spreadsheetId, range, type, formatJson, expr, formatFields, account }) => {
32112
+ const args = ["sheets", "conditional-format", "add", spreadsheetId, range, `--type=${type}`, `--format-json=${formatJson}`];
32113
+ if (expr !== void 0) args.push(`--expr=${expr}`);
32114
+ if (formatFields) args.push(`--format-fields=${formatFields}`);
32115
+ return runOrDiagnose(args, { account });
32116
+ });
32117
+ server2.registerTool("gog_sheets_conditional_format_clear", {
32118
+ 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.",
32119
+ annotations: { destructiveHint: true },
32120
+ inputSchema: {
32121
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
32122
+ sheet: external_exports.string().describe("Sheet name to clear rules from"),
32123
+ index: external_exports.number().optional().describe("0-based rule index to remove"),
32124
+ all: external_exports.boolean().optional().describe("Remove all conditional formatting rules from the sheet"),
32125
+ account: accountParam
32126
+ }
32127
+ }, async ({ spreadsheetId, sheet, index, all, account }) => {
32128
+ const args = ["sheets", "conditional-format", "clear", spreadsheetId, `--sheet=${sheet}`];
32129
+ if (index !== void 0) args.push(`--index=${index}`);
32130
+ if (all) args.push("--all");
32131
+ return runOrDiagnose(args, { account });
32132
+ });
31872
32133
  }
31873
32134
 
31874
32135
  // src/index.ts
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-sheets",
5
5
  "display_name": "gogcli (Sheets)",
6
- "version": "2.2.0",
6
+ "version": "2.4.0",
7
7
  "description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -136,6 +136,10 @@
136
136
  "name": "gog_sheets_insert",
137
137
  "description": "Insert rows or columns into a sheet."
138
138
  },
139
+ {
140
+ "name": "gog_sheets_copy_paste",
141
+ "description": "Copy a range's values/formulas/format to another range (tiles to fill down/across)."
142
+ },
139
143
  {
140
144
  "name": "gog_sheets_merge",
141
145
  "description": "Merge cells in a range."
@@ -207,6 +211,74 @@
207
211
  {
208
212
  "name": "gog_sheets_reorder_tab",
209
213
  "description": "Move a tab to a specific 0-based position."
214
+ },
215
+ {
216
+ "name": "gog_sheets_chart_list",
217
+ "description": "List embedded charts in a spreadsheet."
218
+ },
219
+ {
220
+ "name": "gog_sheets_chart_get",
221
+ "description": "Get a chart's full definition (spec + position) by chart ID."
222
+ },
223
+ {
224
+ "name": "gog_sheets_chart_create",
225
+ "description": "Create an embedded chart from a JSON ChartSpec."
226
+ },
227
+ {
228
+ "name": "gog_sheets_chart_update",
229
+ "description": "Replace a chart's spec by chart ID."
230
+ },
231
+ {
232
+ "name": "gog_sheets_chart_delete",
233
+ "description": "Delete a chart by chart ID."
234
+ },
235
+ {
236
+ "name": "gog_sheets_table_list",
237
+ "description": "List Google Sheets tables in a spreadsheet."
238
+ },
239
+ {
240
+ "name": "gog_sheets_table_get",
241
+ "description": "Get a Google Sheets table by table ID."
242
+ },
243
+ {
244
+ "name": "gog_sheets_table_create",
245
+ "description": "Create a Google Sheets table over a range with typed columns."
246
+ },
247
+ {
248
+ "name": "gog_sheets_table_append",
249
+ "description": "Append data rows to a table."
250
+ },
251
+ {
252
+ "name": "gog_sheets_table_clear",
253
+ "description": "Clear all data rows from a table."
254
+ },
255
+ {
256
+ "name": "gog_sheets_table_delete",
257
+ "description": "Delete a table by table ID."
258
+ },
259
+ {
260
+ "name": "gog_sheets_banding_list",
261
+ "description": "List alternating-color banded ranges."
262
+ },
263
+ {
264
+ "name": "gog_sheets_banding_set",
265
+ "description": "Apply alternating colors to a range."
266
+ },
267
+ {
268
+ "name": "gog_sheets_banding_clear",
269
+ "description": "Remove alternating-color banding by ID or for a whole sheet."
270
+ },
271
+ {
272
+ "name": "gog_sheets_conditional_format_list",
273
+ "description": "List conditional formatting rules."
274
+ },
275
+ {
276
+ "name": "gog_sheets_conditional_format_add",
277
+ "description": "Add a conditional formatting rule to a range."
278
+ },
279
+ {
280
+ "name": "gog_sheets_conditional_format_clear",
281
+ "description": "Remove conditional formatting rules from a sheet."
210
282
  }
211
283
  ],
212
284
  "compatibility": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-sheets",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp-sheets",
5
5
  "description": "Extended Google Sheets MCP server via gogcli — all base tools plus full Sheets support",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp-sheets"
9
9
  },
10
- "version": "2.2.0",
10
+ "version": "2.4.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-sheets",
15
- "version": "2.2.0",
15
+ "version": "2.4.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -178,9 +178,10 @@ export function registerExtraSheetsTools(server: McpServer): void {
178
178
  start: z.number().describe('Start index (0-based)'),
179
179
  count: z.number().optional().describe('Number of rows/columns to insert (default: 1)'),
180
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.'),
181
+ inheritFromBefore: z.boolean().optional().describe('Inherit number format / styling from the row/column before the insertion. Defaults to true with after:true, false otherwise; pass false to inherit from the row/column after the insertion instead. Cannot inherit from before when inserting at the first row/column.'),
181
182
  account: accountParam,
182
183
  },
183
- }, async ({ spreadsheetId, sheet, dimension, start, count, after, account }) => {
184
+ }, async ({ spreadsheetId, sheet, dimension, start, count, after, inheritFromBefore, account }) => {
184
185
  // The underlying `gog sheets insert` treats `start` as 1-based with `--after` shifting +1.
185
186
  // To honor our 0-based `start` and make `after:true` actually shift the insertion to start+1
186
187
  // (issue #42), we send `start+1` when `after:true`. With after:false the CLI's `start-1`
@@ -189,6 +190,25 @@ export function registerExtraSheetsTools(server: McpServer): void {
189
190
  const args = ['sheets', 'insert', spreadsheetId, sheet, dimension, String(effectiveStart)];
190
191
  if (count !== undefined) args.push(`--count=${count}`);
191
192
  if (after) args.push('--after');
193
+ if (inheritFromBefore !== undefined) args.push(`--inherit-from-before=${inheritFromBefore}`);
194
+ return runOrDiagnose(args, { account });
195
+ });
196
+
197
+ server.registerTool('gog_sheets_copy_paste', {
198
+ description: 'Copy a source range\'s values/formulas/format to a destination range via the Sheets CopyPasteRequest. A destination larger than the source tiles the source to fill it — the canonical way to fill formulas down or across with relative references adjusted (aliases: fill, copy-range). Use type to control what is pasted (NORMAL pastes everything; FORMULA fills formulas with adjusted references; VALUES/FORMAT/etc. paste only that aspect).',
199
+ annotations: { destructiveHint: true },
200
+ inputSchema: {
201
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
202
+ source: z.string().describe('Source range (e.g. Sheet1!A2:H71)'),
203
+ dest: z.string().describe('Destination range (e.g. Sheet1!A2:H120). Larger than the source tiles/fills it.'),
204
+ type: z.enum(['NORMAL', 'VALUES', 'FORMAT', 'FORMULA', 'NO_BORDERS', 'DATA_VALIDATION', 'CONDITIONAL_FORMATTING']).optional().describe('Paste type (default: NORMAL pastes everything)'),
205
+ transpose: z.boolean().optional().describe('Paste transposed (swap rows and columns)'),
206
+ account: accountParam,
207
+ },
208
+ }, async ({ spreadsheetId, source, dest, type, transpose, account }) => {
209
+ const args = ['sheets', 'copy-paste', spreadsheetId, source, dest];
210
+ if (type) args.push(`--type=${type}`);
211
+ if (transpose) args.push('--transpose');
192
212
  return runOrDiagnose(args, { account });
193
213
  });
194
214
 
@@ -485,11 +505,11 @@ export function registerExtraSheetsTools(server: McpServer): void {
485
505
  });
486
506
 
487
507
  server.registerTool('gog_sheets_batch_update', {
488
- description: 'Update values in multiple ranges with one Sheets API request. dataJson is a JSON array of {range, values} objects (or "@/path/to/file.json"). Atomic either all ranges update or none do.',
508
+ description: 'Update values in multiple ranges with one Sheets API request. dataJson is a JSON array of {range, values} objects — pass it inline as a literal JSON string. Atomic — either all ranges update or none do. (The CLI also accepts "@/path/to/file.json", but that file is read on the gog server\'s filesystem, not the caller\'s, so inline JSON is the right form for remote MCP callers.)',
489
509
  annotations: { destructiveHint: true },
490
510
  inputSchema: {
491
511
  spreadsheetId: z.string().describe('Spreadsheet ID'),
492
- dataJson: z.string().describe('Value ranges as JSON array, or @file (e.g. [{"range":"Sheet1!A1:B2","values":[["a","b"]]}])'),
512
+ dataJson: z.string().describe('Value ranges as an inline JSON array, e.g. [{"range":"Sheet1!A1:B2","values":[["a","b"]]}]. (An "@/path" form is read on the gog server filesystem, not yours — inline the JSON instead.)'),
493
513
  input: z.enum(['RAW', 'USER_ENTERED']).optional().describe('Value input option (default: USER_ENTERED)'),
494
514
  includeValuesInResponse: z.boolean().optional().describe('Include updated values in the response'),
495
515
  responseRender: z.enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA']).optional().describe('Response value render option'),
@@ -520,4 +540,262 @@ export function registerExtraSheetsTools(server: McpServer): void {
520
540
  { account },
521
541
  );
522
542
  });
543
+
544
+ // ---- Charts (gog 0.19.0) ----
545
+
546
+ server.registerTool('gog_sheets_chart_list', {
547
+ description: 'List embedded charts in a spreadsheet (chartId, type, position).',
548
+ annotations: { readOnlyHint: true },
549
+ inputSchema: {
550
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
551
+ account: accountParam,
552
+ },
553
+ }, async ({ spreadsheetId, account }) => {
554
+ return runOrDiagnose(['sheets', 'chart', 'list', spreadsheetId], { account });
555
+ });
556
+
557
+ server.registerTool('gog_sheets_chart_get', {
558
+ description: 'Get the full definition (spec + position) of a single chart by its numeric chart ID.',
559
+ annotations: { readOnlyHint: true },
560
+ inputSchema: {
561
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
562
+ chartId: z.string().describe('Numeric chart ID (from gog_sheets_chart_list)'),
563
+ account: accountParam,
564
+ },
565
+ }, async ({ spreadsheetId, chartId, account }) => {
566
+ return runOrDiagnose(['sheets', 'chart', 'get', spreadsheetId, chartId], { account });
567
+ });
568
+
569
+ server.registerTool('gog_sheets_chart_create', {
570
+ 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.',
571
+ inputSchema: {
572
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
573
+ specJson: z.string().describe('ChartSpec or EmbeddedChart JSON (inline or @file)'),
574
+ sheet: z.string().optional().describe('Sheet name for the anchor (resolved to sheetId)'),
575
+ anchor: z.string().optional().describe('Anchor cell in A1 notation (e.g. A1, E10)'),
576
+ width: z.number().optional().describe('Chart width in pixels (default: 600)'),
577
+ height: z.number().optional().describe('Chart height in pixels (default: 371)'),
578
+ account: accountParam,
579
+ },
580
+ }, async ({ spreadsheetId, specJson, sheet, anchor, width, height, account }) => {
581
+ const args = ['sheets', 'chart', 'create', spreadsheetId, `--spec-json=${specJson}`];
582
+ if (sheet) args.push(`--sheet=${sheet}`);
583
+ if (anchor) args.push(`--anchor=${anchor}`);
584
+ if (width !== undefined) args.push(`--width=${width}`);
585
+ if (height !== undefined) args.push(`--height=${height}`);
586
+ return runOrDiagnose(args, { account });
587
+ });
588
+
589
+ server.registerTool('gog_sheets_chart_update', {
590
+ description: 'Replace a chart spec by chart ID. specJson is a Sheets API ChartSpec (or full EmbeddedChart) — inline or @/path/to/file.json.',
591
+ annotations: { destructiveHint: true },
592
+ inputSchema: {
593
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
594
+ chartId: z.string().describe('Numeric chart ID to update'),
595
+ specJson: z.string().describe('ChartSpec or EmbeddedChart JSON (inline or @file)'),
596
+ account: accountParam,
597
+ },
598
+ }, async ({ spreadsheetId, chartId, specJson, account }) => {
599
+ return runOrDiagnose(
600
+ ['sheets', 'chart', 'update', spreadsheetId, chartId, `--spec-json=${specJson}`],
601
+ { account },
602
+ );
603
+ });
604
+
605
+ server.registerTool('gog_sheets_chart_delete', {
606
+ description: 'Delete a chart by its numeric chart ID.',
607
+ annotations: { destructiveHint: true },
608
+ inputSchema: {
609
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
610
+ chartId: z.string().describe('Numeric chart ID to delete'),
611
+ account: accountParam,
612
+ },
613
+ }, async ({ spreadsheetId, chartId, account }) => {
614
+ return runOrDiagnose(['sheets', 'chart', 'delete', spreadsheetId, chartId], { account });
615
+ });
616
+
617
+ // ---- Tables (gog 0.19.0) ----
618
+
619
+ server.registerTool('gog_sheets_table_list', {
620
+ description: 'List Google Sheets tables in a spreadsheet (tableId, name, range).',
621
+ annotations: { readOnlyHint: true },
622
+ inputSchema: {
623
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
624
+ account: accountParam,
625
+ },
626
+ }, async ({ spreadsheetId, account }) => {
627
+ return runOrDiagnose(['sheets', 'table', 'list', spreadsheetId], { account });
628
+ });
629
+
630
+ server.registerTool('gog_sheets_table_get', {
631
+ description: 'Get a single Google Sheets table (definition + columns) by its table ID.',
632
+ annotations: { readOnlyHint: true },
633
+ inputSchema: {
634
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
635
+ tableId: z.string().describe('Table ID (from gog_sheets_table_list)'),
636
+ account: accountParam,
637
+ },
638
+ }, async ({ spreadsheetId, tableId, account }) => {
639
+ return runOrDiagnose(['sheets', 'table', 'get', spreadsheetId, tableId], { account });
640
+ });
641
+
642
+ server.registerTool('gog_sheets_table_create', {
643
+ 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.',
644
+ inputSchema: {
645
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
646
+ range: z.string().describe('Range the table covers (e.g. Sheet1!A1:D20)'),
647
+ name: z.string().describe('Table name'),
648
+ columnsJson: z.string().describe('Column definitions as JSON array or @file (columnName + optional columnType)'),
649
+ account: accountParam,
650
+ },
651
+ }, async ({ spreadsheetId, range, name, columnsJson, account }) => {
652
+ return runOrDiagnose(
653
+ ['sheets', 'table', 'create', spreadsheetId, range, `--name=${name}`, `--columns-json=${columnsJson}`],
654
+ { account },
655
+ );
656
+ });
657
+
658
+ server.registerTool('gog_sheets_table_append', {
659
+ description: 'Append data rows to a table. valuesJson is a JSON 2D array of row values.',
660
+ inputSchema: {
661
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
662
+ tableId: z.string().describe('Table ID to append to'),
663
+ valuesJson: z.string().describe('Values as JSON 2D array (e.g. [["a",1],["b",2]])'),
664
+ input: z.enum(['RAW', 'USER_ENTERED']).optional().describe('Value input option (default: USER_ENTERED)'),
665
+ account: accountParam,
666
+ },
667
+ }, async ({ spreadsheetId, tableId, valuesJson, input, account }) => {
668
+ const args = ['sheets', 'table', 'append', spreadsheetId, tableId, `--values-json=${valuesJson}`];
669
+ if (input) args.push(`--input=${input}`);
670
+ return runOrDiagnose(args, { account });
671
+ });
672
+
673
+ server.registerTool('gog_sheets_table_clear', {
674
+ description: 'Clear all data rows from a table (keeps the table and its columns).',
675
+ annotations: { destructiveHint: true },
676
+ inputSchema: {
677
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
678
+ tableId: z.string().describe('Table ID to clear'),
679
+ account: accountParam,
680
+ },
681
+ }, async ({ spreadsheetId, tableId, account }) => {
682
+ return runOrDiagnose(['sheets', 'table', 'clear', spreadsheetId, tableId], { account });
683
+ });
684
+
685
+ server.registerTool('gog_sheets_table_delete', {
686
+ description: 'Delete a table by its table ID.',
687
+ annotations: { destructiveHint: true },
688
+ inputSchema: {
689
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
690
+ tableId: z.string().describe('Table ID to delete'),
691
+ account: accountParam,
692
+ },
693
+ }, async ({ spreadsheetId, tableId, account }) => {
694
+ return runOrDiagnose(['sheets', 'table', 'delete', spreadsheetId, tableId], { account });
695
+ });
696
+
697
+ // ---- Banding / alternating colors (gog 0.19.0) ----
698
+
699
+ server.registerTool('gog_sheets_banding_list', {
700
+ description: 'List alternating-color banded ranges. Optionally scope to a single sheet.',
701
+ annotations: { readOnlyHint: true },
702
+ inputSchema: {
703
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
704
+ sheet: z.string().optional().describe('Only list banding from this sheet'),
705
+ account: accountParam,
706
+ },
707
+ }, async ({ spreadsheetId, sheet, account }) => {
708
+ const args = ['sheets', 'banding', 'list', spreadsheetId];
709
+ if (sheet) args.push(`--sheet=${sheet}`);
710
+ return runOrDiagnose(args, { account });
711
+ });
712
+
713
+ server.registerTool('gog_sheets_banding_set', {
714
+ 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.',
715
+ inputSchema: {
716
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
717
+ range: z.string().describe('Range to band (e.g. Sheet1!A1:D20)'),
718
+ rowPropertiesJson: z.string().optional().describe('BandingProperties JSON for row colors'),
719
+ columnPropertiesJson: z.string().optional().describe('BandingProperties JSON for column colors'),
720
+ account: accountParam,
721
+ },
722
+ }, async ({ spreadsheetId, range, rowPropertiesJson, columnPropertiesJson, account }) => {
723
+ const args = ['sheets', 'banding', 'set', spreadsheetId, range];
724
+ if (rowPropertiesJson) args.push(`--row-properties-json=${rowPropertiesJson}`);
725
+ if (columnPropertiesJson) args.push(`--column-properties-json=${columnPropertiesJson}`);
726
+ return runOrDiagnose(args, { account });
727
+ });
728
+
729
+ server.registerTool('gog_sheets_banding_clear', {
730
+ 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.',
731
+ annotations: { destructiveHint: true },
732
+ inputSchema: {
733
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
734
+ id: z.number().optional().describe('Banded range ID to remove'),
735
+ all: z.boolean().optional().describe('Remove all banding from the sheet (requires sheet)'),
736
+ sheet: z.string().optional().describe('Sheet name (used with all:true)'),
737
+ account: accountParam,
738
+ },
739
+ }, async ({ spreadsheetId, id, all, sheet, account }) => {
740
+ const args = ['sheets', 'banding', 'clear', spreadsheetId];
741
+ if (id !== undefined) args.push(`--id=${id}`);
742
+ if (all) args.push('--all');
743
+ if (sheet) args.push(`--sheet=${sheet}`);
744
+ return runOrDiagnose(args, { account });
745
+ });
746
+
747
+ // ---- Conditional formatting (gog 0.19.0) ----
748
+
749
+ server.registerTool('gog_sheets_conditional_format_list', {
750
+ description: 'List conditional formatting rules. Optionally scope to a single sheet.',
751
+ annotations: { readOnlyHint: true },
752
+ inputSchema: {
753
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
754
+ sheet: z.string().optional().describe('Only list rules from this sheet'),
755
+ account: accountParam,
756
+ },
757
+ }, async ({ spreadsheetId, sheet, account }) => {
758
+ const args = ['sheets', 'conditional-format', 'list', spreadsheetId];
759
+ if (sheet) args.push(`--sheet=${sheet}`);
760
+ return runOrDiagnose(args, { account });
761
+ });
762
+
763
+ server.registerTool('gog_sheets_conditional_format_add', {
764
+ 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).',
765
+ inputSchema: {
766
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
767
+ range: z.string().describe('Range the rule applies to (e.g. Sheet1!A1:A100)'),
768
+ type: z.enum([
769
+ 'text-eq', 'text-contains', 'text-starts-with', 'text-ends-with',
770
+ 'number-eq', 'number-gt', 'number-gte', 'number-lt', 'number-lte',
771
+ 'blank', 'not-blank', 'custom-formula',
772
+ ]).describe('Rule type'),
773
+ formatJson: z.string().describe('CellFormat JSON to apply when the condition matches (inline or @file)'),
774
+ expr: z.string().optional().describe('Expression value or custom formula (omit for blank/not-blank)'),
775
+ formatFields: z.string().optional().describe('Format field mask for force-sending zero/false fields (e.g. backgroundColor,textFormat.bold)'),
776
+ account: accountParam,
777
+ },
778
+ }, async ({ spreadsheetId, range, type, formatJson, expr, formatFields, account }) => {
779
+ const args = ['sheets', 'conditional-format', 'add', spreadsheetId, range, `--type=${type}`, `--format-json=${formatJson}`];
780
+ if (expr !== undefined) args.push(`--expr=${expr}`);
781
+ if (formatFields) args.push(`--format-fields=${formatFields}`);
782
+ return runOrDiagnose(args, { account });
783
+ });
784
+
785
+ server.registerTool('gog_sheets_conditional_format_clear', {
786
+ 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.',
787
+ annotations: { destructiveHint: true },
788
+ inputSchema: {
789
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
790
+ sheet: z.string().describe('Sheet name to clear rules from'),
791
+ index: z.number().optional().describe('0-based rule index to remove'),
792
+ all: z.boolean().optional().describe('Remove all conditional formatting rules from the sheet'),
793
+ account: accountParam,
794
+ },
795
+ }, async ({ spreadsheetId, sheet, index, all, account }) => {
796
+ const args = ['sheets', 'conditional-format', 'clear', spreadsheetId, `--sheet=${sheet}`];
797
+ if (index !== undefined) args.push(`--index=${index}`);
798
+ if (all) args.push('--all');
799
+ return runOrDiagnose(args, { account });
800
+ });
523
801
  }
@@ -149,6 +149,43 @@ describe('gog_sheets_insert', () => {
149
149
  await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 0, after: false });
150
150
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'ROWS', '0'], { account: undefined });
151
151
  });
152
+
153
+ it('passes --inherit-from-before=true when set', async () => {
154
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
155
+ const handlers = setupHandlers();
156
+ await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 5, after: true, inheritFromBefore: true });
157
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'ROWS', '6', '--after', '--inherit-from-before=true'], { account: undefined });
158
+ });
159
+
160
+ it('passes --inherit-from-before=false to inherit from the following neighbor', async () => {
161
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
162
+ const handlers = setupHandlers();
163
+ await handlers.get('gog_sheets_insert')!({ spreadsheetId: 'sid', sheet: 'S1', dimension: 'ROWS', start: 5, after: true, inheritFromBefore: false });
164
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'insert', 'sid', 'S1', 'ROWS', '6', '--after', '--inherit-from-before=false'], { account: undefined });
165
+ });
166
+ });
167
+
168
+ describe('gog_sheets_copy_paste', () => {
169
+ it('calls runOrDiagnose with required source/dest', async () => {
170
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
171
+ const handlers = setupHandlers();
172
+ await handlers.get('gog_sheets_copy_paste')!({ spreadsheetId: 'sid', source: 'Sheet1!A2:H71', dest: 'Sheet1!A2:H120' });
173
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'copy-paste', 'sid', 'Sheet1!A2:H71', 'Sheet1!A2:H120'], { account: undefined });
174
+ });
175
+
176
+ it('appends --type and --transpose when provided', async () => {
177
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
178
+ const handlers = setupHandlers();
179
+ await handlers.get('gog_sheets_copy_paste')!({ spreadsheetId: 'sid', source: 'A1:B2', dest: 'D1:E2', type: 'FORMULA', transpose: true });
180
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'copy-paste', 'sid', 'A1:B2', 'D1:E2', '--type=FORMULA', '--transpose'], { account: undefined });
181
+ });
182
+
183
+ it('forwards account', async () => {
184
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
185
+ const handlers = setupHandlers();
186
+ await handlers.get('gog_sheets_copy_paste')!({ spreadsheetId: 'sid', source: 'A1', dest: 'B1', account: 'a@b.com' });
187
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'copy-paste', 'sid', 'A1', 'B1'], { account: 'a@b.com' });
188
+ });
152
189
  });
153
190
 
154
191
  // 8. merge
@@ -487,6 +524,18 @@ describe('gog_sheets_number_format', () => {
487
524
  expect(result.content[0].text).toMatch(/warning/i);
488
525
  });
489
526
 
527
+ it('does not warn when every cell is null or empty (no small integers seen)', async () => {
528
+ const handlers = setupHandlers();
529
+ vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
530
+ if (args[1] === 'get') {
531
+ return toText(JSON.stringify({ range: 'A1:A3', values: [[null], [''], [null]] }));
532
+ }
533
+ return toText('{"ok":true}');
534
+ });
535
+ const result = await handlers.get('gog_sheets_number_format')!({ spreadsheetId: 'sid', range: 'A1:A3', type: 'DATE' });
536
+ expect(result.content[0].text).not.toMatch(/warning/i);
537
+ });
538
+
490
539
  it('proceeds with format when peek fails (does not block)', async () => {
491
540
  const handlers = setupHandlers();
492
541
  vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
@@ -775,3 +824,325 @@ describe('gog_sheets_reorder_tab', () => {
775
824
  );
776
825
  });
777
826
  });
827
+
828
+ // 25. chart list (gog 0.19.0)
829
+ describe('gog_sheets_chart_list', () => {
830
+ it('calls runOrDiagnose with correct args', async () => {
831
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
832
+ const handlers = setupHandlers();
833
+ await handlers.get('gog_sheets_chart_list')!({ spreadsheetId: 'sid' });
834
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'chart', 'list', 'sid'], { account: undefined });
835
+ });
836
+
837
+ it('forwards account', async () => {
838
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
839
+ const handlers = setupHandlers();
840
+ await handlers.get('gog_sheets_chart_list')!({ spreadsheetId: 'sid', account: 'a@b.com' });
841
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'chart', 'list', 'sid'], { account: 'a@b.com' });
842
+ });
843
+ });
844
+
845
+ // 26. chart get
846
+ describe('gog_sheets_chart_get', () => {
847
+ it('calls runOrDiagnose with correct args', async () => {
848
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
849
+ const handlers = setupHandlers();
850
+ await handlers.get('gog_sheets_chart_get')!({ spreadsheetId: 'sid', chartId: '12345' });
851
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'chart', 'get', 'sid', '12345'], { account: undefined });
852
+ });
853
+ });
854
+
855
+ // 27. chart create
856
+ describe('gog_sheets_chart_create', () => {
857
+ it('calls runOrDiagnose with only --spec-json', async () => {
858
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
859
+ const handlers = setupHandlers();
860
+ const spec = '{"basicChart":{"chartType":"COLUMN"}}';
861
+ await handlers.get('gog_sheets_chart_create')!({ spreadsheetId: 'sid', specJson: spec });
862
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
863
+ ['sheets', 'chart', 'create', 'sid', `--spec-json=${spec}`],
864
+ { account: undefined },
865
+ );
866
+ });
867
+
868
+ it('includes --sheet, --anchor, --width, --height when provided', async () => {
869
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
870
+ const handlers = setupHandlers();
871
+ await handlers.get('gog_sheets_chart_create')!({
872
+ spreadsheetId: 'sid', specJson: '{}', sheet: 'Data', anchor: 'E10', width: 800, height: 400,
873
+ });
874
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
875
+ ['sheets', 'chart', 'create', 'sid', '--spec-json={}', '--sheet=Data', '--anchor=E10', '--width=800', '--height=400'],
876
+ { account: undefined },
877
+ );
878
+ });
879
+
880
+ it('includes --width=0 and --height=0 when zero', async () => {
881
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
882
+ const handlers = setupHandlers();
883
+ await handlers.get('gog_sheets_chart_create')!({ spreadsheetId: 'sid', specJson: '{}', width: 0, height: 0 });
884
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
885
+ ['sheets', 'chart', 'create', 'sid', '--spec-json={}', '--width=0', '--height=0'],
886
+ { account: undefined },
887
+ );
888
+ });
889
+ });
890
+
891
+ // 28. chart update
892
+ describe('gog_sheets_chart_update', () => {
893
+ it('calls runOrDiagnose with chartId and --spec-json', async () => {
894
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
895
+ const handlers = setupHandlers();
896
+ await handlers.get('gog_sheets_chart_update')!({ spreadsheetId: 'sid', chartId: '99', specJson: '{}' });
897
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
898
+ ['sheets', 'chart', 'update', 'sid', '99', '--spec-json={}'],
899
+ { account: undefined },
900
+ );
901
+ });
902
+ });
903
+
904
+ // 29. chart delete
905
+ describe('gog_sheets_chart_delete', () => {
906
+ it('calls runOrDiagnose with correct args', async () => {
907
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
908
+ const handlers = setupHandlers();
909
+ await handlers.get('gog_sheets_chart_delete')!({ spreadsheetId: 'sid', chartId: '7' });
910
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'chart', 'delete', 'sid', '7'], { account: undefined });
911
+ });
912
+ });
913
+
914
+ // 30. table list
915
+ describe('gog_sheets_table_list', () => {
916
+ it('calls runOrDiagnose with correct args', async () => {
917
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
918
+ const handlers = setupHandlers();
919
+ await handlers.get('gog_sheets_table_list')!({ spreadsheetId: 'sid' });
920
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'table', 'list', 'sid'], { account: undefined });
921
+ });
922
+ });
923
+
924
+ // 31. table get
925
+ describe('gog_sheets_table_get', () => {
926
+ it('calls runOrDiagnose with correct args', async () => {
927
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
928
+ const handlers = setupHandlers();
929
+ await handlers.get('gog_sheets_table_get')!({ spreadsheetId: 'sid', tableId: 't1' });
930
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'table', 'get', 'sid', 't1'], { account: undefined });
931
+ });
932
+ });
933
+
934
+ // 32. table create
935
+ describe('gog_sheets_table_create', () => {
936
+ it('calls runOrDiagnose with range, --name, --columns-json', async () => {
937
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
938
+ const handlers = setupHandlers();
939
+ const cols = '[{"columnName":"Name","columnType":"TEXT"}]';
940
+ await handlers.get('gog_sheets_table_create')!({ spreadsheetId: 'sid', range: 'Sheet1!A1:B10', name: 'People', columnsJson: cols });
941
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
942
+ ['sheets', 'table', 'create', 'sid', 'Sheet1!A1:B10', '--name=People', `--columns-json=${cols}`],
943
+ { account: undefined },
944
+ );
945
+ });
946
+ });
947
+
948
+ // 33. table append
949
+ describe('gog_sheets_table_append', () => {
950
+ it('calls runOrDiagnose with --values-json (no --input)', async () => {
951
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
952
+ const handlers = setupHandlers();
953
+ await handlers.get('gog_sheets_table_append')!({ spreadsheetId: 'sid', tableId: 't1', valuesJson: '[["a",1]]' });
954
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
955
+ ['sheets', 'table', 'append', 'sid', 't1', '--values-json=[["a",1]]'],
956
+ { account: undefined },
957
+ );
958
+ });
959
+
960
+ it('includes --input when provided', async () => {
961
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
962
+ const handlers = setupHandlers();
963
+ await handlers.get('gog_sheets_table_append')!({ spreadsheetId: 'sid', tableId: 't1', valuesJson: '[]', input: 'RAW' });
964
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
965
+ ['sheets', 'table', 'append', 'sid', 't1', '--values-json=[]', '--input=RAW'],
966
+ { account: undefined },
967
+ );
968
+ });
969
+ });
970
+
971
+ // 34. table clear
972
+ describe('gog_sheets_table_clear', () => {
973
+ it('calls runOrDiagnose with correct args', async () => {
974
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
975
+ const handlers = setupHandlers();
976
+ await handlers.get('gog_sheets_table_clear')!({ spreadsheetId: 'sid', tableId: 't1' });
977
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'table', 'clear', 'sid', 't1'], { account: undefined });
978
+ });
979
+ });
980
+
981
+ // 35. table delete
982
+ describe('gog_sheets_table_delete', () => {
983
+ it('calls runOrDiagnose with correct args', async () => {
984
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
985
+ const handlers = setupHandlers();
986
+ await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1' });
987
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'table', 'delete', 'sid', 't1'], { account: undefined });
988
+ });
989
+ });
990
+
991
+ // 36. banding list
992
+ describe('gog_sheets_banding_list', () => {
993
+ it('calls runOrDiagnose with no --sheet', async () => {
994
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
995
+ const handlers = setupHandlers();
996
+ await handlers.get('gog_sheets_banding_list')!({ spreadsheetId: 'sid' });
997
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'list', 'sid'], { account: undefined });
998
+ });
999
+
1000
+ it('includes --sheet when provided', async () => {
1001
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1002
+ const handlers = setupHandlers();
1003
+ await handlers.get('gog_sheets_banding_list')!({ spreadsheetId: 'sid', sheet: 'Data' });
1004
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'list', 'sid', '--sheet=Data'], { account: undefined });
1005
+ });
1006
+ });
1007
+
1008
+ // 37. banding set
1009
+ describe('gog_sheets_banding_set', () => {
1010
+ it('calls runOrDiagnose with range only', async () => {
1011
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1012
+ const handlers = setupHandlers();
1013
+ await handlers.get('gog_sheets_banding_set')!({ spreadsheetId: 'sid', range: 'Sheet1!A1:D20' });
1014
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'set', 'sid', 'Sheet1!A1:D20'], { account: undefined });
1015
+ });
1016
+
1017
+ it('includes --row-properties-json and --column-properties-json when provided', async () => {
1018
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1019
+ const handlers = setupHandlers();
1020
+ await handlers.get('gog_sheets_banding_set')!({
1021
+ spreadsheetId: 'sid', range: 'A1:D20',
1022
+ rowPropertiesJson: '{"firstBandColor":{}}',
1023
+ columnPropertiesJson: '{"secondBandColor":{}}',
1024
+ });
1025
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1026
+ ['sheets', 'banding', 'set', 'sid', 'A1:D20', '--row-properties-json={"firstBandColor":{}}', '--column-properties-json={"secondBandColor":{}}'],
1027
+ { account: undefined },
1028
+ );
1029
+ });
1030
+ });
1031
+
1032
+ // 38. banding clear
1033
+ describe('gog_sheets_banding_clear', () => {
1034
+ it('calls runOrDiagnose with no optional flags', async () => {
1035
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1036
+ const handlers = setupHandlers();
1037
+ await handlers.get('gog_sheets_banding_clear')!({ spreadsheetId: 'sid' });
1038
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid'], { account: undefined });
1039
+ });
1040
+
1041
+ it('includes --id when provided (including 0)', async () => {
1042
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1043
+ const handlers = setupHandlers();
1044
+ await handlers.get('gog_sheets_banding_clear')!({ spreadsheetId: 'sid', id: 0 });
1045
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid', '--id=0'], { account: undefined });
1046
+ });
1047
+
1048
+ it('includes --all and --sheet when clearing a whole sheet', async () => {
1049
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1050
+ const handlers = setupHandlers();
1051
+ await handlers.get('gog_sheets_banding_clear')!({ spreadsheetId: 'sid', all: true, sheet: 'Data' });
1052
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid', '--all', '--sheet=Data'], { account: undefined });
1053
+ });
1054
+
1055
+ it('omits --all when false', async () => {
1056
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1057
+ const handlers = setupHandlers();
1058
+ await handlers.get('gog_sheets_banding_clear')!({ spreadsheetId: 'sid', all: false, id: 5 });
1059
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'banding', 'clear', 'sid', '--id=5'], { account: undefined });
1060
+ });
1061
+ });
1062
+
1063
+ // 39. conditional-format list
1064
+ describe('gog_sheets_conditional_format_list', () => {
1065
+ it('calls runOrDiagnose with no --sheet', async () => {
1066
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1067
+ const handlers = setupHandlers();
1068
+ await handlers.get('gog_sheets_conditional_format_list')!({ spreadsheetId: 'sid' });
1069
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'list', 'sid'], { account: undefined });
1070
+ });
1071
+
1072
+ it('includes --sheet when provided', async () => {
1073
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1074
+ const handlers = setupHandlers();
1075
+ await handlers.get('gog_sheets_conditional_format_list')!({ spreadsheetId: 'sid', sheet: 'Data' });
1076
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'list', 'sid', '--sheet=Data'], { account: undefined });
1077
+ });
1078
+ });
1079
+
1080
+ // 40. conditional-format add
1081
+ describe('gog_sheets_conditional_format_add', () => {
1082
+ it('calls runOrDiagnose with type and --format-json (no expr/fields)', async () => {
1083
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1084
+ const handlers = setupHandlers();
1085
+ await handlers.get('gog_sheets_conditional_format_add')!({
1086
+ spreadsheetId: 'sid', range: 'A1:A100', type: 'not-blank', formatJson: '{"backgroundColor":{}}',
1087
+ });
1088
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1089
+ ['sheets', 'conditional-format', 'add', 'sid', 'A1:A100', '--type=not-blank', '--format-json={"backgroundColor":{}}'],
1090
+ { account: undefined },
1091
+ );
1092
+ });
1093
+
1094
+ it('includes --expr and --format-fields when provided', async () => {
1095
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1096
+ const handlers = setupHandlers();
1097
+ await handlers.get('gog_sheets_conditional_format_add')!({
1098
+ spreadsheetId: 'sid', range: 'B1:B50', type: 'number-gt', expr: '100', formatJson: '{}', formatFields: 'backgroundColor,textFormat.bold',
1099
+ });
1100
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1101
+ ['sheets', 'conditional-format', 'add', 'sid', 'B1:B50', '--type=number-gt', '--format-json={}', '--expr=100', '--format-fields=backgroundColor,textFormat.bold'],
1102
+ { account: undefined },
1103
+ );
1104
+ });
1105
+
1106
+ it('includes --expr when it is an empty string', async () => {
1107
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1108
+ const handlers = setupHandlers();
1109
+ await handlers.get('gog_sheets_conditional_format_add')!({
1110
+ spreadsheetId: 'sid', range: 'A1', type: 'text-eq', expr: '', formatJson: '{}',
1111
+ });
1112
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1113
+ ['sheets', 'conditional-format', 'add', 'sid', 'A1', '--type=text-eq', '--format-json={}', '--expr='],
1114
+ { account: undefined },
1115
+ );
1116
+ });
1117
+ });
1118
+
1119
+ // 41. conditional-format clear
1120
+ describe('gog_sheets_conditional_format_clear', () => {
1121
+ it('calls runOrDiagnose with --sheet only', async () => {
1122
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1123
+ const handlers = setupHandlers();
1124
+ await handlers.get('gog_sheets_conditional_format_clear')!({ spreadsheetId: 'sid', sheet: 'Data' });
1125
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data'], { account: undefined });
1126
+ });
1127
+
1128
+ it('includes --index when provided (including 0)', async () => {
1129
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1130
+ const handlers = setupHandlers();
1131
+ await handlers.get('gog_sheets_conditional_format_clear')!({ spreadsheetId: 'sid', sheet: 'Data', index: 0 });
1132
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data', '--index=0'], { account: undefined });
1133
+ });
1134
+
1135
+ it('includes --all when true', async () => {
1136
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1137
+ const handlers = setupHandlers();
1138
+ await handlers.get('gog_sheets_conditional_format_clear')!({ spreadsheetId: 'sid', sheet: 'Data', all: true });
1139
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data', '--all'], { account: undefined });
1140
+ });
1141
+
1142
+ it('omits --all when false', async () => {
1143
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1144
+ const handlers = setupHandlers();
1145
+ await handlers.get('gog_sheets_conditional_format_clear')!({ spreadsheetId: 'sid', sheet: 'Data', all: false, index: 2 });
1146
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data', '--index=2'], { account: undefined });
1147
+ });
1148
+ });