gogcli-mcp-sheets 4.5.2 → 4.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.
@@ -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": "4.5.2"
10
+ "version": "4.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": "4.5.2",
18
+ "version": "4.6.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": "4.5.2",
4
+ "version": "4.6.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
@@ -36369,6 +36369,9 @@ function countNonEmptyCells(getOutput) {
36369
36369
  var DATASOURCE_ALIASES = /* @__PURE__ */ new Set(["datasource", "data-source", "data-sources", "connected-sheets"]);
36370
36370
  var DATASOURCE_BILLED = /* @__PURE__ */ new Set(["add", "update", "refresh"]);
36371
36371
  function vetSheetsRun(subcommand, args) {
36372
+ if (subcommand.toLowerCase() === "batch-request") {
36373
+ return gatedElsewhere("gog sheets batch-request", "gog_sheets_run", "applies structural changes to a spreadsheet", "gog_sheets_batch_request");
36374
+ }
36372
36375
  if (!DATASOURCE_ALIASES.has(subcommand.toLowerCase())) return void 0;
36373
36376
  const word = hasCommandWord(args, DATASOURCE_BILLED)?.toLowerCase();
36374
36377
  return word ? gatedElsewhere(`gog sheets datasource ${word}`, "gog_sheets_run", "runs a billed BigQuery job", `gog_sheets_datasource_${word}`) : void 0;
@@ -36492,7 +36495,7 @@ function registerSheetsTools(server) {
36492
36495
  }
36493
36496
 
36494
36497
  // ../gogcli-mcp/src/server.ts
36495
- var VERSION = true ? "4.5.2" : "0.0.0";
36498
+ var VERSION = true ? "4.6.0" : "0.0.0";
36496
36499
 
36497
36500
  // ../gogcli-mcp/src/bootstrap-auth.ts
36498
36501
  import { join as join5 } from "node:path";
@@ -36655,6 +36658,35 @@ async function confirmBilledExecution(ctx, o) {
36655
36658
  });
36656
36659
  }
36657
36660
  function registerExtraSheetsTools(server) {
36661
+ server.registerTool("gog_sheets_batch_begin", {
36662
+ description: "Open a persisted, revision-locked Sheets request batch. Pass its returned batchId to gog_sheets_batch_request using batch=<id>; no spreadsheet changes are submitted until gog_sheets_batch_end.",
36663
+ annotations: { destructiveHint: false },
36664
+ inputSchema: external_exports.object({ spreadsheetId: external_exports.string().describe("Spreadsheet ID the batch is locked to"), name: external_exports.string().optional().describe("Optional batch label"), account: accountParam })
36665
+ }, async ({ spreadsheetId, name, account }) => {
36666
+ const args = ["batch", "begin", `--service=sheets`, `--spreadsheet=${spreadsheetId}`];
36667
+ if (name) args.push(`--name=${name}`);
36668
+ return runOrDiagnose(args, { account });
36669
+ });
36670
+ server.registerTool("gog_sheets_batch_end", {
36671
+ description: "Submit a persisted Sheets request batch atomically against its locked revision. The queued requests are shown for review and require confirmation before they are applied.",
36672
+ annotations: { destructiveHint: true },
36673
+ inputSchema: external_exports.object({ batchId: external_exports.string().describe("Batch ID returned by gog_sheets_batch_begin"), account: accountParam, confirmToken: confirmTokenParam })
36674
+ }, async ({ batchId, account, confirmToken }, ctx) => {
36675
+ const previewResult = await runOrDiagnose(["batch", "show", pos(batchId)], { account });
36676
+ if (previewResult.isError) return previewResult;
36677
+ const preview = resultText2(previewResult);
36678
+ if (preview === void 0) throw new Error("gog returned no queued batch preview; refusing to submit an unreviewed batch.");
36679
+ if (Buffer.byteLength(preview, "utf8") > 1e5) throw new Error("The queued batch is too large to review safely through this tool; inspect and submit it with gog directly.");
36680
+ const confirmation = await requireDispatchConfirmation(ctx, {
36681
+ action: "sheets.batch-end",
36682
+ message: "Review and confirm applying the queued Sheets requests:",
36683
+ confirmationLabel: "Confirm applying this atomic Sheets batch.",
36684
+ details: { batchId, queuedBatch: preview },
36685
+ fallback: { tool: "gog_sheets_batch_end", account, confirmToken, subject: () => ({ target: batchId, payload: { queuedBatch: preview }, preview: { batchId, queuedBatch: preview } }) }
36686
+ });
36687
+ if (confirmation) return confirmation;
36688
+ return runOrDiagnose(["batch", "end", pos(batchId), "--force"], { account });
36689
+ });
36658
36690
  server.registerTool("gog_sheets_list_tabs", {
36659
36691
  description: "List tabs (sheets) in a spreadsheet with their titles, sheetIds, and indices. A friendlier view than gog_sheets_metadata when you only need the tab list \u2014 useful for restructuring a workbook over a long agent session without losing track of names.",
36660
36692
  annotations: { readOnlyHint: true },
@@ -37180,6 +37212,54 @@ ${resultText2(result) ?? ""}` }] };
37180
37212
  if (responseDateTimeRender) args.push(`--response-date-time-render=${responseDateTimeRender}`);
37181
37213
  return runOrDiagnose(args, { account });
37182
37214
  });
37215
+ server.registerTool("gog_sheets_batch_request", {
37216
+ description: "Submit a Google Sheets API structural request array atomically (for example addDimension, repeatCell, mergeCells, or updateSheetProperties). requestsJson must be a JSON array of Sheets API Request objects. Requests are previewed and require confirmation before submission. Use batch=<id> to append these requests to a persisted Sheets batch instead of submitting immediately.",
37217
+ annotations: { destructiveHint: true },
37218
+ inputSchema: external_exports.object({
37219
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
37220
+ requestsJson: external_exports.string().describe('JSON array of Google Sheets API Request objects, e.g. [{"addSheet":{"properties":{"title":"Archive"}}}]'),
37221
+ batch: external_exports.string().optional().describe("Persisted Sheets batch ID; append requests without submitting yet"),
37222
+ account: accountParam,
37223
+ confirmToken: confirmTokenParam
37224
+ })
37225
+ }, async ({ spreadsheetId, requestsJson, batch, account, confirmToken }, ctx) => {
37226
+ confineAtFile(requestsJson, "requestsJson");
37227
+ let requests;
37228
+ try {
37229
+ requests = JSON.parse(requestsJson);
37230
+ } catch {
37231
+ throw new Error("requestsJson must be valid JSON");
37232
+ }
37233
+ if (!Array.isArray(requests) || requests.length === 0) throw new Error("requestsJson must be a non-empty JSON array");
37234
+ if (Buffer.byteLength(requestsJson, "utf8") > 1e5) throw new Error("requestsJson exceeds the 100 KB inline limit; split the requests across persisted batches");
37235
+ const args = ["sheets", "batch-request", pos(spreadsheetId), `--requests-json=${requestsJson}`];
37236
+ if (batch) {
37237
+ args.push(`--batch=${batch}`);
37238
+ return runOrDiagnose(args, { account });
37239
+ }
37240
+ const got = await runOrDiagnose(["sheets", "metadata", pos(spreadsheetId), "--select=properties.title"], { account });
37241
+ if (got.isError) return got;
37242
+ const title = spreadsheetTitle(resultText2(got));
37243
+ const confirmation = await requireDispatchConfirmation(ctx, {
37244
+ action: "sheets.batch-request",
37245
+ message: "Review and confirm these structural changes to the spreadsheet:",
37246
+ confirmationLabel: "Confirm submitting this atomic Sheets request batch.",
37247
+ details: {
37248
+ spreadsheet: { id: spreadsheetId, ...title !== void 0 ? { title } : {} },
37249
+ requestCount: requests.length,
37250
+ requests
37251
+ },
37252
+ fallback: {
37253
+ tool: "gog_sheets_batch_request",
37254
+ account,
37255
+ confirmToken,
37256
+ subject: () => ({ target: spreadsheetId, payload: { requests }, preview: { requestCount: requests.length, requests } })
37257
+ }
37258
+ });
37259
+ if (confirmation) return confirmation;
37260
+ args.push("--force");
37261
+ return runOrDiagnose(args, { account });
37262
+ });
37183
37263
  server.registerTool("gog_sheets_reorder_tab", {
37184
37264
  description: "Move a tab to a specific 0-based position. `tab` is the tab name or numeric sheetId; `to=0` is the leftmost slot.",
37185
37265
  annotations: { destructiveHint: true },
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": "4.5.2",
6
+ "version": "4.6.0",
7
7
  "description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -362,6 +362,18 @@
362
362
  {
363
363
  "name": "gog_sheets_datasource_delete",
364
364
  "description": "Delete a Connected Sheets data source and its linked sheet"
365
+ },
366
+ {
367
+ "name": "gog_sheets_batch_request",
368
+ "description": "Submit an atomic array of Google Sheets structural requests after confirmation."
369
+ },
370
+ {
371
+ "name": "gog_sheets_batch_begin",
372
+ "description": "Open a revision-locked Sheets request batch."
373
+ },
374
+ {
375
+ "name": "gog_sheets_batch_end",
376
+ "description": "Review and atomically submit a Sheets request batch after confirmation."
365
377
  }
366
378
  ],
367
379
  "compatibility": {
package/mint.yaml CHANGED
@@ -92,7 +92,7 @@ dependencies:
92
92
  # bumping these nine pins, and the pin stored on each mcp-host registration.
93
93
  - kind: github-release
94
94
  repo: openclaw/gogcli
95
- tag: v0.41.0
95
+ tag: v0.43.0
96
96
  asset: "gogcli_*_linux_amd64.tar.gz"
97
97
  bin: [gog]
98
98
  state:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-sheets",
3
- "version": "4.5.2",
3
+ "version": "4.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": "4.5.2",
10
+ "version": "4.6.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-sheets",
15
- "version": "4.5.2",
15
+ "version": "4.6.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -166,6 +166,37 @@ async function confirmBilledExecution(
166
166
 
167
167
  export function registerExtraSheetsTools(server: McpServer): void {
168
168
 
169
+ server.registerTool('gog_sheets_batch_begin', {
170
+ description: 'Open a persisted, revision-locked Sheets request batch. Pass its returned batchId to gog_sheets_batch_request using batch=<id>; no spreadsheet changes are submitted until gog_sheets_batch_end.',
171
+ annotations: { destructiveHint: false },
172
+ inputSchema: z.object({ spreadsheetId: z.string().describe('Spreadsheet ID the batch is locked to'), name: z.string().optional().describe('Optional batch label'), account: accountParam }),
173
+ }, async ({ spreadsheetId, name, account }) => {
174
+ const args: GogArg[] = ['batch', 'begin', `--service=sheets`, `--spreadsheet=${spreadsheetId}`];
175
+ if (name) args.push(`--name=${name}`);
176
+ return runOrDiagnose(args, { account });
177
+ });
178
+
179
+ server.registerTool('gog_sheets_batch_end', {
180
+ description: 'Submit a persisted Sheets request batch atomically against its locked revision. The queued requests are shown for review and require confirmation before they are applied.',
181
+ annotations: { destructiveHint: true },
182
+ inputSchema: z.object({ batchId: z.string().describe('Batch ID returned by gog_sheets_batch_begin'), account: accountParam, confirmToken: confirmTokenParam }),
183
+ }, async ({ batchId, account, confirmToken }, ctx) => {
184
+ const previewResult = await runOrDiagnose(['batch', 'show', pos(batchId)], { account });
185
+ if (previewResult.isError) return previewResult;
186
+ const preview = resultText(previewResult);
187
+ if (preview === undefined) throw new Error('gog returned no queued batch preview; refusing to submit an unreviewed batch.');
188
+ if (Buffer.byteLength(preview, 'utf8') > 100_000) throw new Error('The queued batch is too large to review safely through this tool; inspect and submit it with gog directly.');
189
+ const confirmation = await requireDispatchConfirmation(ctx, {
190
+ action: 'sheets.batch-end',
191
+ message: 'Review and confirm applying the queued Sheets requests:',
192
+ confirmationLabel: 'Confirm applying this atomic Sheets batch.',
193
+ details: { batchId, queuedBatch: preview },
194
+ fallback: { tool: 'gog_sheets_batch_end', account, confirmToken, subject: () => ({ target: batchId, payload: { queuedBatch: preview }, preview: { batchId, queuedBatch: preview } }) },
195
+ });
196
+ if (confirmation) return confirmation;
197
+ return runOrDiagnose(['batch', 'end', pos(batchId), '--force'], { account });
198
+ });
199
+
169
200
  server.registerTool('gog_sheets_list_tabs', {
170
201
  description: 'List tabs (sheets) in a spreadsheet with their titles, sheetIds, and indices. A friendlier view than gog_sheets_metadata when you only need the tab list — useful for restructuring a workbook over a long agent session without losing track of names.',
171
202
  annotations: { readOnlyHint: true },
@@ -734,6 +765,49 @@ export function registerExtraSheetsTools(server: McpServer): void {
734
765
  return runOrDiagnose(args, { account });
735
766
  });
736
767
 
768
+ server.registerTool('gog_sheets_batch_request', {
769
+ description: 'Submit a Google Sheets API structural request array atomically (for example addDimension, repeatCell, mergeCells, or updateSheetProperties). requestsJson must be a JSON array of Sheets API Request objects. Requests are previewed and require confirmation before submission. Use batch=<id> to append these requests to a persisted Sheets batch instead of submitting immediately.',
770
+ annotations: { destructiveHint: true },
771
+ inputSchema: z.object({
772
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
773
+ requestsJson: z.string().describe('JSON array of Google Sheets API Request objects, e.g. [{"addSheet":{"properties":{"title":"Archive"}}}]'),
774
+ batch: z.string().optional().describe('Persisted Sheets batch ID; append requests without submitting yet'),
775
+ account: accountParam,
776
+ confirmToken: confirmTokenParam,
777
+ }),
778
+ }, async ({ spreadsheetId, requestsJson, batch, account, confirmToken }, ctx) => {
779
+ confineAtFile(requestsJson, 'requestsJson');
780
+ let requests: unknown;
781
+ try { requests = JSON.parse(requestsJson); } catch { throw new Error('requestsJson must be valid JSON'); }
782
+ if (!Array.isArray(requests) || requests.length === 0) throw new Error('requestsJson must be a non-empty JSON array');
783
+ if (Buffer.byteLength(requestsJson, 'utf8') > 100_000) throw new Error('requestsJson exceeds the 100 KB inline limit; split the requests across persisted batches');
784
+ const args: GogArg[] = ['sheets', 'batch-request', pos(spreadsheetId), `--requests-json=${requestsJson}`];
785
+ if (batch) {
786
+ args.push(`--batch=${batch}`);
787
+ return runOrDiagnose(args, { account });
788
+ }
789
+ const got = await runOrDiagnose(['sheets', 'metadata', pos(spreadsheetId), '--select=properties.title'], { account });
790
+ if (got.isError) return got;
791
+ const title = spreadsheetTitle(resultText(got));
792
+ const confirmation = await requireDispatchConfirmation(ctx, {
793
+ action: 'sheets.batch-request',
794
+ message: 'Review and confirm these structural changes to the spreadsheet:',
795
+ confirmationLabel: 'Confirm submitting this atomic Sheets request batch.',
796
+ details: {
797
+ spreadsheet: { id: spreadsheetId, ...(title !== undefined ? { title } : {}) },
798
+ requestCount: requests.length,
799
+ requests,
800
+ },
801
+ fallback: {
802
+ tool: 'gog_sheets_batch_request', account, confirmToken,
803
+ subject: () => ({ target: spreadsheetId, payload: { requests }, preview: { requestCount: requests.length, requests } }),
804
+ },
805
+ });
806
+ if (confirmation) return confirmation;
807
+ args.push('--force');
808
+ return runOrDiagnose(args, { account });
809
+ });
810
+
737
811
  server.registerTool('gog_sheets_reorder_tab', {
738
812
  description: 'Move a tab to a specific 0-based position. `tab` is the tab name or numeric sheetId; `to=0` is the leftmost slot.',
739
813
  annotations: { destructiveHint: true },
@@ -894,6 +894,121 @@ describe('gog_sheets_batch_update', () => {
894
894
  });
895
895
  });
896
896
 
897
+ describe('gog_sheets_batch_request', () => {
898
+ it('opens a revision-locked Sheets batch', async () => {
899
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
900
+ const harness = await setupHandlers();
901
+ await harness.callTool('gog_sheets_batch_begin', { spreadsheetId: 'sid', name: 'quarterly' });
902
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
903
+ ['batch', 'begin', '--service=sheets', '--spreadsheet=sid', '--name=quarterly'], { account: undefined });
904
+ await harness.callTool('gog_sheets_batch_begin', { spreadsheetId: 'sid' });
905
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
906
+ ['batch', 'begin', '--service=sheets', '--spreadsheet=sid'], { account: undefined });
907
+ });
908
+
909
+ it('shows and confirms the queued requests before ending a batch', async () => {
910
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{"requests":[{"addSheet":{}}]}'));
911
+ const harness = await setupHandlers();
912
+ await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1' });
913
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['batch', 'show', pos('batch-1')], { account: undefined });
914
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['batch', 'end', pos('batch-1'), '--force'], { account: undefined });
915
+ });
916
+
917
+ it('does not end a batch when the preview fails or is too large', async () => {
918
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue({ isError: true, content: [{ type: 'text', text: 'failed' }] });
919
+ const harness = await setupHandlers();
920
+ expect((await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1' })).isError).toBe(true);
921
+ expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
922
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('x'.repeat(100_001)));
923
+ expect((await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1' })).isError).toBe(true);
924
+ expect(lib.runOrDiagnose).toHaveBeenCalledTimes(2);
925
+ });
926
+
927
+ it('refuses to submit when gog returns no batch preview', async () => {
928
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue({ content: [] });
929
+ const harness = await setupHandlers();
930
+ expect((await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1' })).isError).toBe(true);
931
+ expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
932
+ });
933
+
934
+ it('confirms an unprompted client using a one-time token bound to the queued requests', async () => {
935
+ process.env.MCP_CONFIRM_MODE = 'ask-user';
936
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{"requests":[{"addSheet":{}}]}'));
937
+ const harness = await createTestHarness(registerExtraSheetsTools);
938
+ const preview = await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1' });
939
+ const payload = JSON.parse(preview.content[0]!.text!);
940
+ await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1', confirmToken: payload.confirmToken });
941
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['batch', 'end', pos('batch-1'), '--force'], { account: undefined });
942
+ delete process.env.MCP_CONFIRM_MODE;
943
+ });
944
+
945
+ it('confirms and submits a structural request array atomically', async () => {
946
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{"properties":{"title":"Plan"}}'));
947
+ const harness = await setupHandlers();
948
+ const requestsJson = '[{"addSheet":{"properties":{"title":"Archive"}}}]';
949
+ await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson });
950
+ expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(1,
951
+ ['sheets', 'metadata', pos('sid'), '--select=properties.title'], { account: undefined });
952
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
953
+ ['sheets', 'batch-request', pos('sid'), `--requests-json=${requestsJson}`, '--force'], { account: undefined });
954
+ });
955
+
956
+ it('appends requests to a persisted batch without submitting or prompting', async () => {
957
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
958
+ const harness = await setupHandlers();
959
+ const requestsJson = '[{"addSheet":{"properties":{"title":"Archive"}}}]';
960
+ await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson, batch: 'batch-1' });
961
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
962
+ ['sheets', 'batch-request', pos('sid'), `--requests-json=${requestsJson}`, '--batch=batch-1'], { account: undefined });
963
+ });
964
+
965
+ it('rejects malformed or empty request arrays before calling gog', async () => {
966
+ const harness = await setupHandlers();
967
+ const malformed = await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson: '{' });
968
+ expect(malformed.isError).toBe(true);
969
+ const empty = await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson: '[]' });
970
+ expect(empty.isError).toBe(true);
971
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
972
+ });
973
+
974
+ it('rejects request payloads that exceed the safe argv size', async () => {
975
+ const harness = await setupHandlers();
976
+ const result = await harness.callTool('gog_sheets_batch_request', {
977
+ spreadsheetId: 'sid', requestsJson: JSON.stringify([{ updateCells: { userEnteredValue: { stringValue: 'x'.repeat(100_001) } } }]),
978
+ });
979
+ expect(result.isError).toBe(true);
980
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
981
+ });
982
+
983
+ it('does not submit if the title lookup fails', async () => {
984
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue({ isError: true, content: [{ type: 'text', text: 'failed' }] });
985
+ const harness = await setupHandlers();
986
+ await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson: '[{"addSheet":{}}]' });
987
+ expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
988
+ });
989
+
990
+ it('confirms by spreadsheet ID when metadata has no title', async () => {
991
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
992
+ const harness = await setupHandlers();
993
+ await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson: '[{"addSheet":{}}]' });
994
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
995
+ ['sheets', 'batch-request', pos('sid'), '--requests-json=[{"addSheet":{}}]', '--force'], { account: undefined });
996
+ });
997
+
998
+ it('uses a one-time token when the client cannot prompt', async () => {
999
+ process.env.MCP_CONFIRM_MODE = 'ask-user';
1000
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{"properties":{"title":"Plan"}}'));
1001
+ const harness = await createTestHarness(registerExtraSheetsTools);
1002
+ const args = { spreadsheetId: 'sid', requestsJson: '[{"addSheet":{"properties":{"title":"Archive"}}}]' };
1003
+ const preview = await harness.callTool('gog_sheets_batch_request', args);
1004
+ const payload = JSON.parse(preview.content[0]!.text!);
1005
+ await harness.callTool('gog_sheets_batch_request', { ...args, confirmToken: payload.confirmToken });
1006
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1007
+ ['sheets', 'batch-request', pos('sid'), `--requests-json=${args.requestsJson}`, '--force'], { account: undefined });
1008
+ delete process.env.MCP_CONFIRM_MODE;
1009
+ });
1010
+ });
1011
+
897
1012
  // 24. reorder-tab (gog 0.18.0)
898
1013
  describe('gog_sheets_reorder_tab', () => {
899
1014
  it('passes --tab and --to', async () => {