gogcli-mcp-sheets 2.4.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -0
- package/SKILL.md +2 -0
- package/dist/index.js +74 -4
- package/manifest.json +10 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/tools/sheets-extra.ts +101 -4
- package/tests/tools/sheets-extra.test.ts +126 -3
|
@@ -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.
|
|
10
|
+
"version": "2.6.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.
|
|
18
|
+
"version": "2.6.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/README.md
CHANGED
|
@@ -72,6 +72,8 @@ Plus 5 auth tools and 8 base Sheets tools (get, update, append, clear, metadata,
|
|
|
72
72
|
| `gog_sheets_notes` | Read cell notes |
|
|
73
73
|
| `gog_sheets_update_note` | Add or update cell notes |
|
|
74
74
|
| `gog_sheets_links` | List hyperlinks in a range |
|
|
75
|
+
| `gog_sheets_set_links` | Set =HYPERLINK() cells in one call (batch) |
|
|
76
|
+
| `gog_sheets_snapshot` | Back up a whole spreadsheet before a risky edit |
|
|
75
77
|
|
|
76
78
|
## License
|
|
77
79
|
|
package/SKILL.md
CHANGED
|
@@ -51,6 +51,8 @@ Extended Google Sheets MCP server via [gogcli](https://github.com/openclaw/gogcl
|
|
|
51
51
|
| `gog_sheets_notes` | Read cell notes |
|
|
52
52
|
| `gog_sheets_update_note` | Add or update cell notes |
|
|
53
53
|
| `gog_sheets_links` | List hyperlinks in a range |
|
|
54
|
+
| `gog_sheets_set_links` | Set =HYPERLINK() cells in one call (batch) |
|
|
55
|
+
| `gog_sheets_snapshot` | Back up a whole spreadsheet before a risky edit |
|
|
54
56
|
| `gog_sheets_named_ranges_list` | List named ranges |
|
|
55
57
|
| `gog_sheets_named_ranges_get` | Get a named range |
|
|
56
58
|
| `gog_sheets_named_ranges_add` | Create a named range |
|
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.
|
|
31402
|
+
var VERSION = true ? "2.6.0" : "0.0.0";
|
|
31403
31403
|
function createServer(options) {
|
|
31404
31404
|
return new McpServer({
|
|
31405
31405
|
name: options?.name ?? "gogcli",
|
|
@@ -31794,6 +31794,27 @@ ${result.content[0].text}` }] };
|
|
|
31794
31794
|
}, async ({ spreadsheetId, range, account }) => {
|
|
31795
31795
|
return runOrDiagnose(["sheets", "links", spreadsheetId, range], { account });
|
|
31796
31796
|
});
|
|
31797
|
+
server2.registerTool("gog_sheets_links_set", {
|
|
31798
|
+
description: "Set cell hyperlinks in a Google Sheet. Three modes: (1) single link \u2014 pass cell + url (+ optional text); (2) multi-link cell \u2014 pass cell + runsJson, a JSON array of rich-text runs (a run with an empty uri is plain text); (3) batch \u2014 pass cellsJson, a JSON array of {cell,url,text} or {cell,runs:[...]} objects written in one request.",
|
|
31799
|
+
annotations: { destructiveHint: true },
|
|
31800
|
+
inputSchema: {
|
|
31801
|
+
spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
|
|
31802
|
+
cell: external_exports.string().optional().describe("Target cell in A1 notation (e.g. Sheet1!B2). Used by single-link and runsJson modes; omit for batch (cellsJson)."),
|
|
31803
|
+
url: external_exports.string().optional().describe("Hyperlink URL for single-link mode"),
|
|
31804
|
+
text: external_exports.string().optional().describe("Display text for single-link mode (defaults to the URL when omitted)"),
|
|
31805
|
+
runsJson: external_exports.string().optional().describe('Multi-link cell: JSON array of runs, e.g. [{"text":"Act A","uri":"https://a"},{"text":" / "},{"text":"Act B","uri":"https://b"}]. A run with an empty uri is plain text.'),
|
|
31806
|
+
cellsJson: external_exports.string().optional().describe("Batch: JSON array of {cell,url,text} or {cell,runs:[{text,uri}]} objects, written in one request."),
|
|
31807
|
+
account: accountParam
|
|
31808
|
+
}
|
|
31809
|
+
}, async ({ spreadsheetId, cell, url: url2, text, runsJson, cellsJson, account }) => {
|
|
31810
|
+
const args = ["sheets", "links", "set", spreadsheetId];
|
|
31811
|
+
if (cell) args.push(cell);
|
|
31812
|
+
if (url2) args.push(url2);
|
|
31813
|
+
if (text) args.push(text);
|
|
31814
|
+
if (runsJson) args.push(`--runs-json=${runsJson}`);
|
|
31815
|
+
if (cellsJson) args.push(`--cells-json=${cellsJson}`);
|
|
31816
|
+
return runOrDiagnose(args, { account });
|
|
31817
|
+
});
|
|
31797
31818
|
server2.registerTool("gog_sheets_named_ranges_list", {
|
|
31798
31819
|
description: "List all named ranges in a spreadsheet.",
|
|
31799
31820
|
annotations: { readOnlyHint: true },
|
|
@@ -32016,15 +32037,51 @@ ${result.content[0].text}` }] };
|
|
|
32016
32037
|
return runOrDiagnose(["sheets", "table", "clear", spreadsheetId, tableId], { account });
|
|
32017
32038
|
});
|
|
32018
32039
|
server2.registerTool("gog_sheets_table_delete", {
|
|
32019
|
-
description: "Delete a table by
|
|
32040
|
+
description: "Delete a Google Sheets table. WARNING: the underlying `gog`/Sheets behaviour is that deleting a table also DESTROYS every cell value in the table range \u2014 not just the table styling/columns. By default this tool prevents that data loss by emulating the Sheets UI's \"Convert to range\": it reads the table's cells (values AND formulas) first, deletes the table, then restores the data into the now-plain range. Formatting, banding, and data-validation dropdowns are still lost (gog cannot round-trip those yet). Set keep_data=false to delete the table AND wipe its cell data (the raw destructive behaviour). To preserve everything, snapshot the whole spreadsheet first with gog_sheets_snapshot.",
|
|
32020
32041
|
annotations: { destructiveHint: true },
|
|
32021
32042
|
inputSchema: {
|
|
32022
32043
|
spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
|
|
32023
32044
|
tableId: external_exports.string().describe("Table ID to delete"),
|
|
32045
|
+
keep_data: external_exports.boolean().optional().describe(
|
|
32046
|
+
`Default true: preserve the table's cell values and formulas (read \u2192 delete \u2192 restore), emulating "Convert to range". Set false to delete the table AND its data.`
|
|
32047
|
+
),
|
|
32024
32048
|
account: accountParam
|
|
32025
32049
|
}
|
|
32026
|
-
}, async ({ spreadsheetId, tableId, account }) => {
|
|
32027
|
-
|
|
32050
|
+
}, async ({ spreadsheetId, tableId, keep_data = true, account }) => {
|
|
32051
|
+
if (!keep_data) {
|
|
32052
|
+
return runOrDiagnose(["sheets", "table", "delete", spreadsheetId, tableId, "--discard-data", "--force"], { account });
|
|
32053
|
+
}
|
|
32054
|
+
let range;
|
|
32055
|
+
let values;
|
|
32056
|
+
try {
|
|
32057
|
+
const parsed = JSON.parse(await run(["sheets", "table", "get", spreadsheetId, tableId], { account }));
|
|
32058
|
+
range = parsed.table?.a1;
|
|
32059
|
+
if (!range) throw new Error(`could not determine the table's range (no "a1" in table get output) for table ${tableId}`);
|
|
32060
|
+
const read = JSON.parse(await run(["sheets", "get", spreadsheetId, range, "--render=FORMULA"], { account }));
|
|
32061
|
+
values = read.values ?? [];
|
|
32062
|
+
} catch (err) {
|
|
32063
|
+
return diagnose(err);
|
|
32064
|
+
}
|
|
32065
|
+
try {
|
|
32066
|
+
await run(["sheets", "table", "delete", spreadsheetId, tableId, "--discard-data", "--force"], { account });
|
|
32067
|
+
} catch (err) {
|
|
32068
|
+
return diagnose(err);
|
|
32069
|
+
}
|
|
32070
|
+
if (values.length > 0) {
|
|
32071
|
+
try {
|
|
32072
|
+
await run(["sheets", "update", spreadsheetId, range, `--values-json=${JSON.stringify(values)}`], { account });
|
|
32073
|
+
} catch (err) {
|
|
32074
|
+
return toText(
|
|
32075
|
+
`Table ${tableId} was deleted, but restoring its data FAILED \u2014 re-apply the values below manually.
|
|
32076
|
+
|
|
32077
|
+
Range: ${range}
|
|
32078
|
+
Values: ${JSON.stringify(values)}
|
|
32079
|
+
|
|
32080
|
+
${toError(err).content[0].text}`
|
|
32081
|
+
);
|
|
32082
|
+
}
|
|
32083
|
+
}
|
|
32084
|
+
return toText(`Deleted table ${tableId} and preserved ${values.length} row(s) of data (values + formulas) in ${range}.`);
|
|
32028
32085
|
});
|
|
32029
32086
|
server2.registerTool("gog_sheets_banding_list", {
|
|
32030
32087
|
description: "List alternating-color banded ranges. Optionally scope to a single sheet.",
|
|
@@ -32130,6 +32187,19 @@ ${result.content[0].text}` }] };
|
|
|
32130
32187
|
if (all) args.push("--all");
|
|
32131
32188
|
return runOrDiagnose(args, { account });
|
|
32132
32189
|
});
|
|
32190
|
+
server2.registerTool("gog_sheets_snapshot", {
|
|
32191
|
+
description: "Make a backup copy of an entire spreadsheet \u2014 a one-call safety snapshot to take BEFORE a risky or destructive edit (table delete, bulk clear, large rewrite). Returns the new copy's file ID and URL; if the edit goes wrong, restore by copying the backup back or sharing it. The copy is independent \u2014 later edits to the original do not affect it.",
|
|
32192
|
+
inputSchema: {
|
|
32193
|
+
spreadsheetId: external_exports.string().describe("Spreadsheet ID to back up"),
|
|
32194
|
+
name: external_exports.string().describe('Name for the backup copy, e.g. "Budget \u2014 backup before table delete"'),
|
|
32195
|
+
parent: external_exports.string().optional().describe("Destination folder ID for the copy (default: same location as the original)"),
|
|
32196
|
+
account: accountParam
|
|
32197
|
+
}
|
|
32198
|
+
}, async ({ spreadsheetId, name, parent, account }) => {
|
|
32199
|
+
const args = ["drive", "copy", spreadsheetId, name];
|
|
32200
|
+
if (parent) args.push(`--parent=${parent}`);
|
|
32201
|
+
return runOrDiagnose(args, { account });
|
|
32202
|
+
});
|
|
32133
32203
|
}
|
|
32134
32204
|
|
|
32135
32205
|
// 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.
|
|
6
|
+
"version": "2.6.0",
|
|
7
7
|
"description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -180,6 +180,10 @@
|
|
|
180
180
|
"name": "gog_sheets_links",
|
|
181
181
|
"description": "List hyperlinks in a range."
|
|
182
182
|
},
|
|
183
|
+
{
|
|
184
|
+
"name": "gog_sheets_links_set",
|
|
185
|
+
"description": "Set native cell hyperlinks: single link, multi-link rich-text cell (runsJson), or batch (cellsJson)."
|
|
186
|
+
},
|
|
183
187
|
{
|
|
184
188
|
"name": "gog_sheets_named_ranges_list",
|
|
185
189
|
"description": "List all named ranges in a spreadsheet."
|
|
@@ -254,7 +258,11 @@
|
|
|
254
258
|
},
|
|
255
259
|
{
|
|
256
260
|
"name": "gog_sheets_table_delete",
|
|
257
|
-
"description": "Delete a table
|
|
261
|
+
"description": "Delete a Google Sheets table. By default preserves the table's cell values and formulas (emulates \"Convert to range\"); set keep_data=false to also wipe the data."
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"name": "gog_sheets_snapshot",
|
|
265
|
+
"description": "Make a backup copy of an entire spreadsheet before a risky/destructive edit. Returns the copy's file ID and URL."
|
|
258
266
|
},
|
|
259
267
|
{
|
|
260
268
|
"name": "gog_sheets_banding_list",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-sheets",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.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.
|
|
10
|
+
"version": "2.6.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp-sheets",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.6.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { accountParam, runOrDiagnose } from '../../../gogcli-mcp/src/lib.js';
|
|
3
|
+
import { accountParam, runOrDiagnose, run, diagnose, toText, toError } from '../../../gogcli-mcp/src/lib.js';
|
|
4
4
|
|
|
5
5
|
// Convert a CSS-style hex color ("#FFF5D9", "#FD9", "FFF5D9") to the
|
|
6
6
|
// {red, green, blue} 0-1 float triple that Sheets API CellFormat expects.
|
|
@@ -440,6 +440,31 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
440
440
|
return runOrDiagnose(['sheets', 'links', spreadsheetId, range], { account });
|
|
441
441
|
});
|
|
442
442
|
|
|
443
|
+
server.registerTool('gog_sheets_links_set', {
|
|
444
|
+
description:
|
|
445
|
+
'Set cell hyperlinks in a Google Sheet. Three modes: (1) single link — pass cell + url (+ optional text); ' +
|
|
446
|
+
'(2) multi-link cell — pass cell + runsJson, a JSON array of rich-text runs (a run with an empty uri is plain text); ' +
|
|
447
|
+
'(3) batch — pass cellsJson, a JSON array of {cell,url,text} or {cell,runs:[...]} objects written in one request.',
|
|
448
|
+
annotations: { destructiveHint: true },
|
|
449
|
+
inputSchema: {
|
|
450
|
+
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
451
|
+
cell: z.string().optional().describe('Target cell in A1 notation (e.g. Sheet1!B2). Used by single-link and runsJson modes; omit for batch (cellsJson).'),
|
|
452
|
+
url: z.string().optional().describe('Hyperlink URL for single-link mode'),
|
|
453
|
+
text: z.string().optional().describe('Display text for single-link mode (defaults to the URL when omitted)'),
|
|
454
|
+
runsJson: z.string().optional().describe('Multi-link cell: JSON array of runs, e.g. [{"text":"Act A","uri":"https://a"},{"text":" / "},{"text":"Act B","uri":"https://b"}]. A run with an empty uri is plain text.'),
|
|
455
|
+
cellsJson: z.string().optional().describe('Batch: JSON array of {cell,url,text} or {cell,runs:[{text,uri}]} objects, written in one request.'),
|
|
456
|
+
account: accountParam,
|
|
457
|
+
},
|
|
458
|
+
}, async ({ spreadsheetId, cell, url, text, runsJson, cellsJson, account }) => {
|
|
459
|
+
const args = ['sheets', 'links', 'set', spreadsheetId];
|
|
460
|
+
if (cell) args.push(cell);
|
|
461
|
+
if (url) args.push(url);
|
|
462
|
+
if (text) args.push(text);
|
|
463
|
+
if (runsJson) args.push(`--runs-json=${runsJson}`);
|
|
464
|
+
if (cellsJson) args.push(`--cells-json=${cellsJson}`);
|
|
465
|
+
return runOrDiagnose(args, { account });
|
|
466
|
+
});
|
|
467
|
+
|
|
443
468
|
server.registerTool('gog_sheets_named_ranges_list', {
|
|
444
469
|
description: 'List all named ranges in a spreadsheet.',
|
|
445
470
|
annotations: { readOnlyHint: true },
|
|
@@ -683,15 +708,70 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
683
708
|
});
|
|
684
709
|
|
|
685
710
|
server.registerTool('gog_sheets_table_delete', {
|
|
686
|
-
description:
|
|
711
|
+
description:
|
|
712
|
+
'Delete a Google Sheets table. WARNING: the underlying `gog`/Sheets behaviour is that deleting a table also DESTROYS every cell value in the table range — not just the table styling/columns. ' +
|
|
713
|
+
'By default this tool prevents that data loss by emulating the Sheets UI\'s "Convert to range": it reads the table\'s cells (values AND formulas) first, deletes the table, then restores the data into the now-plain range. ' +
|
|
714
|
+
'Formatting, banding, and data-validation dropdowns are still lost (gog cannot round-trip those yet). ' +
|
|
715
|
+
'Set keep_data=false to delete the table AND wipe its cell data (the raw destructive behaviour). To preserve everything, snapshot the whole spreadsheet first with gog_sheets_snapshot.',
|
|
687
716
|
annotations: { destructiveHint: true },
|
|
688
717
|
inputSchema: {
|
|
689
718
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
690
719
|
tableId: z.string().describe('Table ID to delete'),
|
|
720
|
+
keep_data: z.boolean().optional().describe(
|
|
721
|
+
'Default true: preserve the table\'s cell values and formulas (read → delete → restore), emulating "Convert to range". Set false to delete the table AND its data.',
|
|
722
|
+
),
|
|
691
723
|
account: accountParam,
|
|
692
724
|
},
|
|
693
|
-
}, async ({ spreadsheetId, tableId, account }) => {
|
|
694
|
-
|
|
725
|
+
}, async ({ spreadsheetId, tableId, keep_data = true, account }) => {
|
|
726
|
+
if (!keep_data) {
|
|
727
|
+
// Raw destructive delete: removes the table and its cell data. gog ≥ 0.23
|
|
728
|
+
// requires --discard-data to confirm the cell wipe (--force only skips the
|
|
729
|
+
// interactive confirmation; it does not authorize the data loss).
|
|
730
|
+
return runOrDiagnose(['sheets', 'table', 'delete', spreadsheetId, tableId, '--discard-data', '--force'], { account });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// keep_data: emulate "Convert to range". Read the full table range with
|
|
734
|
+
// formulas intact, then delete, then write the data back. If we can't read
|
|
735
|
+
// the data first, refuse to delete — losing the backup is the whole risk.
|
|
736
|
+
let range: string;
|
|
737
|
+
let values: unknown[][];
|
|
738
|
+
try {
|
|
739
|
+
// `gog sheets table get` wraps the table under a top-level "table" key.
|
|
740
|
+
const parsed = JSON.parse(await run(['sheets', 'table', 'get', spreadsheetId, tableId], { account }));
|
|
741
|
+
range = parsed.table?.a1;
|
|
742
|
+
if (!range) throw new Error(`could not determine the table's range (no "a1" in table get output) for table ${tableId}`);
|
|
743
|
+
const read = JSON.parse(await run(['sheets', 'get', spreadsheetId, range, '--render=FORMULA'], { account }));
|
|
744
|
+
values = read.values ?? [];
|
|
745
|
+
} catch (err) {
|
|
746
|
+
return diagnose(err);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
// We have already backed up the cells above, so --discard-data (required
|
|
751
|
+
// by gog ≥ 0.23) is the intended path here — we re-write the data right
|
|
752
|
+
// after.
|
|
753
|
+
await run(['sheets', 'table', 'delete', spreadsheetId, tableId, '--discard-data', '--force'], { account });
|
|
754
|
+
} catch (err) {
|
|
755
|
+
// Table not deleted; cell data is untouched.
|
|
756
|
+
return diagnose(err);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (values.length > 0) {
|
|
760
|
+
try {
|
|
761
|
+
await run(['sheets', 'update', spreadsheetId, range, `--values-json=${JSON.stringify(values)}`], { account });
|
|
762
|
+
} catch (err) {
|
|
763
|
+
// The table is already gone but the restore write failed. Hand the
|
|
764
|
+
// read-back data straight back so it can be re-applied manually rather
|
|
765
|
+
// than silently lost.
|
|
766
|
+
return toText(
|
|
767
|
+
`Table ${tableId} was deleted, but restoring its data FAILED — re-apply the values below manually.\n\n` +
|
|
768
|
+
`Range: ${range}\nValues: ${JSON.stringify(values)}\n\n` +
|
|
769
|
+
`${toError(err).content[0].text}`,
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return toText(`Deleted table ${tableId} and preserved ${values.length} row(s) of data (values + formulas) in ${range}.`);
|
|
695
775
|
});
|
|
696
776
|
|
|
697
777
|
// ---- Banding / alternating colors (gog 0.19.0) ----
|
|
@@ -798,4 +878,21 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
798
878
|
if (all) args.push('--all');
|
|
799
879
|
return runOrDiagnose(args, { account });
|
|
800
880
|
});
|
|
881
|
+
|
|
882
|
+
server.registerTool('gog_sheets_snapshot', {
|
|
883
|
+
description:
|
|
884
|
+
'Make a backup copy of an entire spreadsheet — a one-call safety snapshot to take BEFORE a risky or destructive edit (table delete, bulk clear, large rewrite). ' +
|
|
885
|
+
'Returns the new copy\'s file ID and URL; if the edit goes wrong, restore by copying the backup back or sharing it. The copy is independent — later edits to the original do not affect it.',
|
|
886
|
+
inputSchema: {
|
|
887
|
+
spreadsheetId: z.string().describe('Spreadsheet ID to back up'),
|
|
888
|
+
name: z.string().describe('Name for the backup copy, e.g. "Budget — backup before table delete"'),
|
|
889
|
+
parent: z.string().optional().describe('Destination folder ID for the copy (default: same location as the original)'),
|
|
890
|
+
account: accountParam,
|
|
891
|
+
},
|
|
892
|
+
}, async ({ spreadsheetId, name, parent, account }) => {
|
|
893
|
+
const args = ['drive', 'copy', spreadsheetId, name];
|
|
894
|
+
if (parent) args.push(`--parent=${parent}`);
|
|
895
|
+
return runOrDiagnose(args, { account });
|
|
896
|
+
});
|
|
897
|
+
|
|
801
898
|
}
|
|
@@ -8,6 +8,8 @@ vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
|
|
|
8
8
|
return {
|
|
9
9
|
...actual,
|
|
10
10
|
runOrDiagnose: vi.fn(),
|
|
11
|
+
run: vi.fn(),
|
|
12
|
+
diagnose: vi.fn(),
|
|
11
13
|
};
|
|
12
14
|
});
|
|
13
15
|
|
|
@@ -684,6 +686,39 @@ describe('gog_sheets_links', () => {
|
|
|
684
686
|
});
|
|
685
687
|
});
|
|
686
688
|
|
|
689
|
+
// gog 0.23.0
|
|
690
|
+
describe('gog_sheets_links_set', () => {
|
|
691
|
+
it('sets a single-cell hyperlink with cell/url/text positionals', async () => {
|
|
692
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
693
|
+
const handlers = setupHandlers();
|
|
694
|
+
await handlers.get('gog_sheets_links_set')!({ spreadsheetId: 'sid', cell: 'A1', url: 'https://x.com', text: 'Link' });
|
|
695
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
696
|
+
['sheets', 'links', 'set', 'sid', 'A1', 'https://x.com', 'Link'],
|
|
697
|
+
{ account: undefined },
|
|
698
|
+
);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('sets a multi-link cell with --runs-json', async () => {
|
|
702
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
703
|
+
const handlers = setupHandlers();
|
|
704
|
+
await handlers.get('gog_sheets_links_set')!({ spreadsheetId: 'sid', cell: 'B2', runsJson: '[{"text":"A","uri":"https://a"}]' });
|
|
705
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
706
|
+
['sheets', 'links', 'set', 'sid', 'B2', '--runs-json=[{"text":"A","uri":"https://a"}]'],
|
|
707
|
+
{ account: undefined },
|
|
708
|
+
);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('sets batch hyperlinks with --cells-json', async () => {
|
|
712
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
713
|
+
const handlers = setupHandlers();
|
|
714
|
+
await handlers.get('gog_sheets_links_set')!({ spreadsheetId: 'sid', cellsJson: '[{"cell":"A1","url":"https://a"}]' });
|
|
715
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
716
|
+
['sheets', 'links', 'set', 'sid', '--cells-json=[{"cell":"A1","url":"https://a"}]'],
|
|
717
|
+
{ account: undefined },
|
|
718
|
+
);
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
687
722
|
// 18. named-ranges list
|
|
688
723
|
describe('gog_sheets_named_ranges_list', () => {
|
|
689
724
|
it('calls runOrDiagnose with correct args', async () => {
|
|
@@ -980,11 +1015,82 @@ describe('gog_sheets_table_clear', () => {
|
|
|
980
1015
|
|
|
981
1016
|
// 35. table delete
|
|
982
1017
|
describe('gog_sheets_table_delete', () => {
|
|
983
|
-
it('
|
|
1018
|
+
it('keep_data=false deletes table (and data) directly with --discard-data --force', async () => {
|
|
984
1019
|
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
985
1020
|
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 });
|
|
1021
|
+
await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1', keep_data: false });
|
|
1022
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'table', 'delete', 'sid', 't1', '--discard-data', '--force'], { account: undefined });
|
|
1023
|
+
expect(lib.run).not.toHaveBeenCalled();
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it('keep_data (default) reads formulas, deletes, then restores the data', async () => {
|
|
1027
|
+
vi.mocked(lib.run)
|
|
1028
|
+
.mockResolvedValueOnce('{"table":{"a1":"Sheet1!A1:C2"}}') // table get
|
|
1029
|
+
.mockResolvedValueOnce('{"values":[["Name","City","N"],["A","NYC","=1+1"]]}') // get --render=FORMULA
|
|
1030
|
+
.mockResolvedValueOnce('{"deleted":{"tableId":"t1"}}') // table delete
|
|
1031
|
+
.mockResolvedValueOnce('{"updatedRows":2}'); // update restore
|
|
1032
|
+
const handlers = setupHandlers();
|
|
1033
|
+
const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1', account: 'a@b.com' });
|
|
1034
|
+
expect(lib.run).toHaveBeenNthCalledWith(1, ['sheets', 'table', 'get', 'sid', 't1'], { account: 'a@b.com' });
|
|
1035
|
+
expect(lib.run).toHaveBeenNthCalledWith(2, ['sheets', 'get', 'sid', 'Sheet1!A1:C2', '--render=FORMULA'], { account: 'a@b.com' });
|
|
1036
|
+
expect(lib.run).toHaveBeenNthCalledWith(3, ['sheets', 'table', 'delete', 'sid', 't1', '--discard-data', '--force'], { account: 'a@b.com' });
|
|
1037
|
+
expect(lib.run).toHaveBeenNthCalledWith(4, ['sheets', 'update', 'sid', 'Sheet1!A1:C2', '--values-json=[["Name","City","N"],["A","NYC","=1+1"]]'], { account: 'a@b.com' });
|
|
1038
|
+
expect(res.content[0].text).toContain('2 row');
|
|
1039
|
+
expect(res.content[0].text).toContain('Sheet1!A1:C2');
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it('keep_data with an empty table skips the restore write', async () => {
|
|
1043
|
+
vi.mocked(lib.run)
|
|
1044
|
+
.mockResolvedValueOnce('{"table":{"a1":"Sheet1!A1:C1"}}') // table get
|
|
1045
|
+
.mockResolvedValueOnce('{}') // get --render=FORMULA, no values key
|
|
1046
|
+
.mockResolvedValueOnce('{"deleted":{}}'); // table delete
|
|
1047
|
+
const handlers = setupHandlers();
|
|
1048
|
+
const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1' });
|
|
1049
|
+
expect(lib.run).toHaveBeenCalledTimes(3); // no update call
|
|
1050
|
+
expect(res.content[0].text).toContain('0 row');
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it('aborts without deleting when the table has no a1 range', async () => {
|
|
1054
|
+
vi.mocked(lib.run).mockResolvedValueOnce('{"name":"Repro"}'); // table get, no a1
|
|
1055
|
+
vi.mocked(lib.diagnose).mockResolvedValue(toText('diagnosed'));
|
|
1056
|
+
const handlers = setupHandlers();
|
|
1057
|
+
const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1' });
|
|
1058
|
+
expect(lib.run).toHaveBeenCalledTimes(1); // only the read, no delete
|
|
1059
|
+
expect(res.content[0].text).toBe('diagnosed');
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
it('aborts without deleting when the pre-delete read fails', async () => {
|
|
1063
|
+
vi.mocked(lib.run).mockRejectedValueOnce(new Error('read boom')); // table get throws
|
|
1064
|
+
vi.mocked(lib.diagnose).mockResolvedValue(toText('diagnosed read'));
|
|
1065
|
+
const handlers = setupHandlers();
|
|
1066
|
+
const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1' });
|
|
1067
|
+
expect(lib.run).toHaveBeenCalledTimes(1);
|
|
1068
|
+
expect(res.content[0].text).toBe('diagnosed read');
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it('diagnoses a delete failure leaving the data intact', async () => {
|
|
1072
|
+
vi.mocked(lib.run)
|
|
1073
|
+
.mockResolvedValueOnce('{"table":{"a1":"Sheet1!A1:C2"}}')
|
|
1074
|
+
.mockResolvedValueOnce('{"values":[["A"]]}')
|
|
1075
|
+
.mockRejectedValueOnce(new Error('delete boom')); // table delete throws
|
|
1076
|
+
vi.mocked(lib.diagnose).mockResolvedValue(toText('diagnosed delete'));
|
|
1077
|
+
const handlers = setupHandlers();
|
|
1078
|
+
const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1' });
|
|
1079
|
+
expect(res.content[0].text).toBe('diagnosed delete');
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
it('surfaces the read-back values when restore fails after deletion', async () => {
|
|
1083
|
+
vi.mocked(lib.run)
|
|
1084
|
+
.mockResolvedValueOnce('{"table":{"a1":"Sheet1!A1:C2"}}')
|
|
1085
|
+
.mockResolvedValueOnce('{"values":[["keepme"]]}')
|
|
1086
|
+
.mockResolvedValueOnce('{"deleted":{}}')
|
|
1087
|
+
.mockRejectedValueOnce(new Error('restore boom')); // update throws
|
|
1088
|
+
const handlers = setupHandlers();
|
|
1089
|
+
const res = await handlers.get('gog_sheets_table_delete')!({ spreadsheetId: 'sid', tableId: 't1' });
|
|
1090
|
+
const text = res.content[0].text;
|
|
1091
|
+
expect(text).toContain('restoring its data FAILED');
|
|
1092
|
+
expect(text).toContain('keepme'); // the data is handed back for manual recovery
|
|
1093
|
+
expect(text).toContain('restore boom');
|
|
988
1094
|
});
|
|
989
1095
|
});
|
|
990
1096
|
|
|
@@ -1146,3 +1252,20 @@ describe('gog_sheets_conditional_format_clear', () => {
|
|
|
1146
1252
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'conditional-format', 'clear', 'sid', '--sheet=Data', '--index=2'], { account: undefined });
|
|
1147
1253
|
});
|
|
1148
1254
|
});
|
|
1255
|
+
|
|
1256
|
+
// 49. snapshot (backup before destructive edits)
|
|
1257
|
+
describe('gog_sheets_snapshot', () => {
|
|
1258
|
+
it('copies the spreadsheet to a named backup', async () => {
|
|
1259
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
1260
|
+
const handlers = setupHandlers();
|
|
1261
|
+
await handlers.get('gog_sheets_snapshot')!({ spreadsheetId: 'sid', name: 'Backup A' });
|
|
1262
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'copy', 'sid', 'Backup A'], { account: undefined });
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
it('includes --parent and forwards account', async () => {
|
|
1266
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
1267
|
+
const handlers = setupHandlers();
|
|
1268
|
+
await handlers.get('gog_sheets_snapshot')!({ spreadsheetId: 'sid', name: 'Backup A', parent: 'folder1', account: 'a@b.com' });
|
|
1269
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'copy', 'sid', 'Backup A', '--parent=folder1'], { account: 'a@b.com' });
|
|
1270
|
+
});
|
|
1271
|
+
});
|