gogcli-mcp-docs 2.30.0 → 3.0.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.
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { accountParam, runOrDiagnose, paginationParams, pushPaginationFlags, payloadArg } from '../../../gogcli-mcp/src/lib.js';
4
4
  import type { GogArg } from '../../../gogcli-mcp/src/lib.js';
@@ -6,12 +6,12 @@ import type { GogArg } from '../../../gogcli-mcp/src/lib.js';
6
6
  export function registerExtraDocsTools(server: McpServer): void {
7
7
  server.registerTool('gog_docs_copy', {
8
8
  description: 'Copy a Google Doc to a new document with the given title.',
9
- inputSchema: {
9
+ inputSchema: z.object({
10
10
  docId: z.string().describe('Doc ID (from the URL)'),
11
11
  title: z.string().describe('Title for the new copy'),
12
12
  parent: z.string().optional().describe('Parent folder ID to place the copy in'),
13
13
  account: accountParam,
14
- },
14
+ }),
15
15
  }, async ({ docId, title, parent, account }) => {
16
16
  const args = ['docs', 'copy', docId, title];
17
17
  if (parent) args.push(`--parent=${parent}`);
@@ -21,7 +21,7 @@ export function registerExtraDocsTools(server: McpServer): void {
21
21
  server.registerTool('gog_docs_delete', {
22
22
  description: 'Delete content within a Google Doc by character index range. To remove the entire document (move to Drive trash), use gog_docs_trash.',
23
23
  annotations: { destructiveHint: true },
24
- inputSchema: {
24
+ inputSchema: z.object({
25
25
  docId: z.string().describe('Doc ID (from the URL)'),
26
26
  start: z.number().optional().describe('Start index (character position, 1-based). Required unless `at` is set.'),
27
27
  end: z.number().optional().describe('End index (character position, exclusive). Required unless `at` is set.'),
@@ -32,7 +32,7 @@ export function registerExtraDocsTools(server: McpServer): void {
32
32
  segment: z.string().optional().describe('Target an exact header, footer, or footnote segment ID (header/footer IDs come from gog_docs_header_list / gog_docs_footer_list; footnote segment IDs appear in gog_docs_read json mode) instead of the document body.'),
33
33
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
34
34
  account: accountParam,
35
- },
35
+ }),
36
36
  }, async ({ docId, start, end, at, occurrence, matchCase, tabId, segment, batch, account }) => {
37
37
  const args = ['docs', 'delete'];
38
38
  if (start !== undefined) args.push(`--start=${start}`);
@@ -50,10 +50,10 @@ export function registerExtraDocsTools(server: McpServer): void {
50
50
  server.registerTool('gog_docs_trash', {
51
51
  description: 'Move an entire Google Doc to Drive trash. Convenience wrapper around `gog drive delete` so docs-only users can clean up without installing gogcli-mcp-drive. The doc remains recoverable from Drive trash for ~30 days.',
52
52
  annotations: { destructiveHint: true },
53
- inputSchema: {
53
+ inputSchema: z.object({
54
54
  docId: z.string().describe('Doc ID to move to trash'),
55
55
  account: accountParam,
56
- },
56
+ }),
57
57
  }, async ({ docId, account }) => {
58
58
  return runOrDiagnose(['drive', 'delete', docId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
59
59
  });
@@ -61,13 +61,13 @@ export function registerExtraDocsTools(server: McpServer): void {
61
61
  server.registerTool('gog_docs_edit', {
62
62
  description: 'Edit a Google Doc by finding and replacing text (stream-edit style).',
63
63
  annotations: { destructiveHint: true },
64
- inputSchema: {
64
+ inputSchema: z.object({
65
65
  docId: z.string().describe('Doc ID (from the URL)'),
66
66
  find: z.string().describe('Text to find'),
67
67
  replace: z.string().describe('Replacement text'),
68
68
  matchCase: z.boolean().optional().describe('Case-sensitive matching'),
69
69
  account: accountParam,
70
- },
70
+ }),
71
71
  }, async ({ docId, find, replace, matchCase, account }) => {
72
72
  const args = ['docs', 'edit', docId, find, replace];
73
73
  if (matchCase) args.push('--match-case');
@@ -77,7 +77,7 @@ export function registerExtraDocsTools(server: McpServer): void {
77
77
  server.registerTool('gog_docs_read', {
78
78
  description: 'Read the content of a Google Doc. Default: plain text body. Use format="json" for the raw Google Docs API response (lossless, includes character indices needed for index-based gog_docs_insert / gog_docs_delete calls). For markdown output, use gog_docs_export with format="md" — it writes to a file. Use gog_docs_structure to see paragraph-by-paragraph layout with indices.',
79
79
  annotations: { readOnlyHint: true },
80
- inputSchema: {
80
+ inputSchema: z.object({
81
81
  docId: z.string().describe('Doc ID (from the URL)'),
82
82
  format: z.enum(['text', 'json']).optional().describe('Output format (default: text)'),
83
83
  tab: z.string().optional().describe('Target tab title or ID. In json mode, returns that one tab in the legacy top-level Document shape.'),
@@ -85,7 +85,7 @@ export function registerExtraDocsTools(server: McpServer): void {
85
85
  maxBytes: z.number().optional().describe('Max bytes to read in text mode (0 = unlimited; default 2000000)'),
86
86
  chips: z.boolean().optional().describe('Render Google Docs smart chips (people, dates, rich links) inline in text mode; ignored in json mode'),
87
87
  account: accountParam,
88
- },
88
+ }),
89
89
  }, async ({ docId, format, tab, allTabs, maxBytes, chips, account }) => {
90
90
  if (format === 'json') {
91
91
  const args = ['docs', 'raw', docId, '--pretty'];
@@ -105,7 +105,7 @@ export function registerExtraDocsTools(server: McpServer): void {
105
105
  server.registerTool('gog_docs_format', {
106
106
  description: 'Apply text or paragraph formatting to a Google Doc. Use `match` to format a specific text occurrence, `matchAll` to format every occurrence, or omit both to format the whole doc. Boolean flags (bold/italic/etc.) set the attribute; negated flags (noBold/noItalic/etc.) clear it.',
107
107
  annotations: { destructiveHint: true },
108
- inputSchema: {
108
+ inputSchema: z.object({
109
109
  docId: z.string().describe('Doc ID (from the URL)'),
110
110
  match: z.string().optional().describe('Format only the first text match'),
111
111
  matchAll: z.boolean().optional().describe('Format all matches instead of only the first'),
@@ -145,7 +145,7 @@ export function registerExtraDocsTools(server: McpServer): void {
145
145
  segment: z.string().optional().describe('Target an exact header, footer, or footnote segment ID (header/footer IDs come from gog_docs_header_list / gog_docs_footer_list; footnote segment IDs appear in gog_docs_read json mode) instead of the document body.'),
146
146
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
147
147
  account: accountParam,
148
- },
148
+ }),
149
149
  }, async (args) => {
150
150
  const a = args as {
151
151
  docId: string;
@@ -219,11 +219,11 @@ export function registerExtraDocsTools(server: McpServer): void {
219
219
  server.registerTool('gog_docs_suggestions_list', {
220
220
  description: 'List pending suggestions (suggested text insertions and deletions) in a Google Doc, with exact UTF-16 ranges and segment context. Read-only; does not accept or reject suggestions.',
221
221
  annotations: { readOnlyHint: true },
222
- inputSchema: {
222
+ inputSchema: z.object({
223
223
  docId: z.string().describe('Doc ID (from the URL)'),
224
224
  tab: z.string().optional().describe('Tab title or ID (omit for the first tab)'),
225
225
  account: accountParam,
226
- },
226
+ }),
227
227
  }, async ({ docId, tab, account }) => {
228
228
  const args = ['docs', 'suggestions', 'list', docId];
229
229
  if (tab) args.push(`--tab=${tab}`);
@@ -233,13 +233,13 @@ export function registerExtraDocsTools(server: McpServer): void {
233
233
  server.registerTool('gog_docs_export', {
234
234
  description: 'Export a Google Doc as PDF, plain text, HTML, DOCX, or other format.',
235
235
  annotations: { readOnlyHint: true },
236
- inputSchema: {
236
+ inputSchema: z.object({
237
237
  docId: z.string().describe('Doc ID (from the URL)'),
238
238
  format: z.string().optional().describe('Export format: pdf, txt, html, docx, rtf, odt, epub (default: pdf)'),
239
239
  out: z.string().optional().describe('Output file path'),
240
240
  overwrite: z.boolean().optional().describe('Overwrite the output file if it already exists (gog refuses otherwise)'),
241
241
  account: accountParam,
242
- },
242
+ }),
243
243
  }, async ({ docId, format, out, overwrite, account }) => {
244
244
  const args = ['docs', 'export', docId];
245
245
  if (format) args.push(`--format=${format}`);
@@ -251,7 +251,7 @@ export function registerExtraDocsTools(server: McpServer): void {
251
251
  server.registerTool('gog_docs_insert', {
252
252
  description: 'Insert text at a specific character index in a Google Doc. When `index` is omitted, gog defaults to 1 (the very beginning), NOT the end — sequential inserts without an explicit index produce reversed output. To append at the end of the doc, use gog_docs_append (which uses `gog docs write --append` and is the right tool for iterative document construction). To find a valid index for mid-document inserts, call gog_docs_structure or gog_docs_read first.',
253
253
  annotations: { destructiveHint: true },
254
- inputSchema: {
254
+ inputSchema: z.object({
255
255
  docId: z.string().describe('Doc ID (from the URL)'),
256
256
  content: z.string().optional().describe('Text content to insert'),
257
257
  index: z.number().optional().describe('Character index to insert at (1-based; default: 1 = start of doc). Prefer gog_docs_append when you want to add at the end.'),
@@ -263,7 +263,7 @@ export function registerExtraDocsTools(server: McpServer): void {
263
263
  segment: z.string().optional().describe('Target an exact header, footer, or footnote segment ID (header/footer IDs come from gog_docs_header_list / gog_docs_footer_list; footnote segment IDs appear in gog_docs_read json mode) instead of the document body.'),
264
264
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
265
265
  account: accountParam,
266
- },
266
+ }),
267
267
  }, async ({ docId, content, index, file, at, occurrence, matchCase, tabId, segment, batch, account }) => {
268
268
  const args = ['docs', 'insert', docId];
269
269
  if (content) args.push(content);
@@ -281,14 +281,14 @@ export function registerExtraDocsTools(server: McpServer): void {
281
281
  server.registerTool('gog_docs_append', {
282
282
  description: 'Append text to the end of a Google Doc. This is the right tool for iterative document construction — multiple sequential calls produce content in the order they were called. Use gog_docs_insert only when you need to insert at a specific character position. Known markdown=true limitations (tracked upstream): (a) 3+ tables in one call reorders the trailing punctuation of the paragraph before the 3rd table — split into multiple calls with ≤2 tables each (openclaw/gogcli#607); (b) inline **bold** / *italic* / `code` inside table cells renders as literal characters — pre-format cell text separately or apply formatting after the append via gog_docs_format (openclaw/gogcli#608); (c) tables with an empty header row leak the last data row as literal pipe text — always supply a non-empty header (openclaw/gogcli#609).',
283
283
  annotations: { destructiveHint: true },
284
- inputSchema: {
284
+ inputSchema: z.object({
285
285
  docId: z.string().describe('Doc ID (from the URL)'),
286
286
  text: z.string().optional().describe('Text content to append'),
287
287
  file: z.string().optional().describe('Path to a text file to append, read on the gog server. gog also accepts "-" for stdin, but this server never writes to gog\'s stdin, so "-" would hang until the call times out.'),
288
288
  markdown: z.boolean().optional().describe('Convert markdown to Google Docs formatting (headings, bold, lists, etc.). See the tool description for known upstream limitations around tables.'),
289
289
  tab: z.string().optional().describe('Target tab title or ID (for multi-tab docs)'),
290
290
  account: accountParam,
291
- },
291
+ }),
292
292
  }, async ({ docId, text, file, markdown, tab, account }) => {
293
293
  const args = ['docs', 'write', docId, '--append'];
294
294
  if (text) args.push(`--text=${text}`);
@@ -301,10 +301,10 @@ export function registerExtraDocsTools(server: McpServer): void {
301
301
  server.registerTool('gog_docs_list_tabs', {
302
302
  description: 'List all tabs in a Google Doc.',
303
303
  annotations: { readOnlyHint: true },
304
- inputSchema: {
304
+ inputSchema: z.object({
305
305
  docId: z.string().describe('Doc ID (from the URL)'),
306
306
  account: accountParam,
307
- },
307
+ }),
308
308
  }, async ({ docId, account }) => {
309
309
  return runOrDiagnose(['docs', 'list-tabs', docId], { account });
310
310
  });
@@ -312,14 +312,14 @@ export function registerExtraDocsTools(server: McpServer): void {
312
312
  server.registerTool('gog_docs_sed', {
313
313
  description: 'Stream-edit a Google Doc with sed-like regex expressions (s/find/replace/ syntax).',
314
314
  annotations: { destructiveHint: true },
315
- inputSchema: {
315
+ inputSchema: z.object({
316
316
  docId: z.string().describe('Doc ID (from the URL)'),
317
317
  expression: z.string().optional().describe('Single sed expression (e.g. "s/old/new/g")'),
318
318
  expressions: z.array(z.string()).optional().describe('Multiple sed expressions'),
319
319
  file: z.string().optional().describe('Path to a sed script file'),
320
320
  tab: z.string().optional().describe('Tab name to edit'),
321
321
  account: accountParam,
322
- },
322
+ }),
323
323
  }, async ({ docId, expression, expressions, file, tab, account }) => {
324
324
  const args = ['docs', 'sed', docId];
325
325
  if (expression) args.push(expression);
@@ -336,7 +336,7 @@ export function registerExtraDocsTools(server: McpServer): void {
336
336
  server.registerTool('gog_docs_update', {
337
337
  description: 'Update a Google Doc — insert or replace text at a specific position.',
338
338
  annotations: { destructiveHint: true },
339
- inputSchema: {
339
+ inputSchema: z.object({
340
340
  docId: z.string().describe('Doc ID (from the URL)'),
341
341
  text: z.string().optional().describe('Text content to write'),
342
342
  file: z.string().optional().describe('Path to a file whose content to use'),
@@ -351,7 +351,7 @@ export function registerExtraDocsTools(server: McpServer): void {
351
351
  pageless: z.boolean().optional().describe('Set document to pageless format'),
352
352
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
353
353
  account: accountParam,
354
- },
354
+ }),
355
355
  }, async ({ docId, text, file, index, replaceRange, markdown, at, occurrence, matchCase, tabId, segment, pageless, batch, account }) => {
356
356
  const args = ['docs', 'update', docId];
357
357
  if (text) args.push(`--text=${text}`);
@@ -379,7 +379,7 @@ export function registerExtraDocsTools(server: McpServer): void {
379
379
  'matches[] always spans every tab, so a quote appearing in two tabs is visible as an ambiguity rather than silently resolved. ' +
380
380
  'tab keeps only comments with a match in that tab, which drops orphaned comments and comments that quote nothing.',
381
381
  annotations: { readOnlyHint: true },
382
- inputSchema: {
382
+ inputSchema: z.object({
383
383
  docId: z.string().describe('Doc ID (from the URL)'),
384
384
  includeResolved: z.boolean().optional().describe('Include resolved comments (default: false, open only)'),
385
385
  since: z.string().optional().describe('Only return comments modified at or after this RFC3339 timestamp (e.g. 2026-06-01T00:00:00Z)'),
@@ -387,7 +387,7 @@ export function registerExtraDocsTools(server: McpServer): void {
387
387
  tab: z.string().optional().describe('Only return comments whose quoted text resolves inside this tab (title or ID). Implies locate, and drops orphaned and unquoted comments.'),
388
388
  ...paginationParams,
389
389
  account: accountParam,
390
- },
390
+ }),
391
391
  }, async ({ docId, includeResolved, since, locate, tab, max, page, all, account }) => {
392
392
  const args = ['docs', 'comments', 'list', docId];
393
393
  if (includeResolved) args.push('--include-resolved');
@@ -401,11 +401,11 @@ export function registerExtraDocsTools(server: McpServer): void {
401
401
  server.registerTool('gog_docs_comments_get', {
402
402
  description: 'Get a single comment by ID, including its replies.',
403
403
  annotations: { readOnlyHint: true },
404
- inputSchema: {
404
+ inputSchema: z.object({
405
405
  docId: z.string().describe('Doc ID (from the URL)'),
406
406
  commentId: z.string().describe('Comment ID'),
407
407
  account: accountParam,
408
- },
408
+ }),
409
409
  }, async ({ docId, commentId, account }) => {
410
410
  return runOrDiagnose(['docs', 'comments', 'get', docId, commentId], { account });
411
411
  });
@@ -413,12 +413,12 @@ export function registerExtraDocsTools(server: McpServer): void {
413
413
  server.registerTool('gog_docs_comments_add', {
414
414
  description:
415
415
  'Add a comment to a Google Doc. Optionally attach quoted text that appears as the highlighted passage in the Google Docs UI.',
416
- inputSchema: {
416
+ inputSchema: z.object({
417
417
  docId: z.string().describe('Doc ID (from the URL)'),
418
418
  content: z.string().describe('Comment text'),
419
419
  quoted: z.string().optional().describe('Quoted text to attach to the comment (shown in UIs when available)'),
420
420
  account: accountParam,
421
- },
421
+ }),
422
422
  }, async ({ docId, content, quoted, account }) => {
423
423
  const args = ['docs', 'comments', 'add', docId, content];
424
424
  if (quoted) args.push(`--quoted=${quoted}`);
@@ -427,12 +427,12 @@ export function registerExtraDocsTools(server: McpServer): void {
427
427
 
428
428
  server.registerTool('gog_docs_comments_reply', {
429
429
  description: 'Reply to an existing comment on a Google Doc.',
430
- inputSchema: {
430
+ inputSchema: z.object({
431
431
  docId: z.string().describe('Doc ID (from the URL)'),
432
432
  commentId: z.string().describe('Comment ID to reply to'),
433
433
  content: z.string().describe('Reply text'),
434
434
  account: accountParam,
435
- },
435
+ }),
436
436
  }, async ({ docId, commentId, content, account }) => {
437
437
  return runOrDiagnose(['docs', 'comments', 'reply', docId, commentId, content], { account });
438
438
  });
@@ -440,12 +440,12 @@ export function registerExtraDocsTools(server: McpServer): void {
440
440
  server.registerTool('gog_docs_comments_resolve', {
441
441
  description: 'Resolve a comment (mark as done). Optionally include a closing message.',
442
442
  annotations: { destructiveHint: true },
443
- inputSchema: {
443
+ inputSchema: z.object({
444
444
  docId: z.string().describe('Doc ID (from the URL)'),
445
445
  commentId: z.string().describe('Comment ID to resolve'),
446
446
  message: z.string().optional().describe('Optional message to include when resolving'),
447
447
  account: accountParam,
448
- },
448
+ }),
449
449
  }, async ({ docId, commentId, message, account }) => {
450
450
  const args = ['docs', 'comments', 'resolve', docId, commentId];
451
451
  if (message) args.push(`--message=${message}`);
@@ -455,11 +455,11 @@ export function registerExtraDocsTools(server: McpServer): void {
455
455
  server.registerTool('gog_docs_comments_delete', {
456
456
  description: 'Delete a comment from a Google Doc. This action is permanent.',
457
457
  annotations: { destructiveHint: true },
458
- inputSchema: {
458
+ inputSchema: z.object({
459
459
  docId: z.string().describe('Doc ID (from the URL)'),
460
460
  commentId: z.string().describe('Comment ID to delete'),
461
461
  account: accountParam,
462
- },
462
+ }),
463
463
  }, async ({ docId, commentId, account }) => {
464
464
  // --force: gog refuses this delete under the runner's --no-input without it.
465
465
  return runOrDiagnose(['docs', 'comments', 'delete', docId, commentId, '--force'], { account });
@@ -468,11 +468,11 @@ export function registerExtraDocsTools(server: McpServer): void {
468
468
  server.registerTool('gog_docs_comments_reopen', {
469
469
  description: 'Reopen a previously resolved comment (flip resolved → open).',
470
470
  annotations: { destructiveHint: true },
471
- inputSchema: {
471
+ inputSchema: z.object({
472
472
  docId: z.string().describe('Doc ID (from the URL)'),
473
473
  commentId: z.string().describe('Comment ID to reopen'),
474
474
  account: accountParam,
475
- },
475
+ }),
476
476
  }, async ({ docId, commentId, account }) => {
477
477
  return runOrDiagnose(['docs', 'comments', 'reopen', docId, commentId], { account });
478
478
  });
@@ -480,14 +480,14 @@ export function registerExtraDocsTools(server: McpServer): void {
480
480
  server.registerTool('gog_docs_comments_locate', {
481
481
  description: 'Locate a comment\'s anchor in a Google Doc — resolves the comment\'s quoted text to its current Docs API index range, or reports the comment as orphaned if the quote can no longer be found (e.g. the anchored text was edited away). Read-only; useful before an index-based edit near a comment.',
482
482
  annotations: { readOnlyHint: true },
483
- inputSchema: {
483
+ inputSchema: z.object({
484
484
  docId: z.string().describe('Doc ID (from the URL)'),
485
485
  commentId: z.string().describe('Comment ID to locate'),
486
486
  matchCase: z.boolean().optional().describe('Case-sensitive matching of the comment quote'),
487
487
  normalizeWhitespace: z.boolean().optional().describe('Collapse whitespace while matching the comment quote'),
488
488
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
489
489
  account: accountParam,
490
- },
490
+ }),
491
491
  }, async ({ docId, commentId, matchCase, normalizeWhitespace, tab, account }) => {
492
492
  const args = ['docs', 'comments', 'locate', docId, commentId];
493
493
  if (matchCase) args.push('--match-case');
@@ -499,7 +499,7 @@ export function registerExtraDocsTools(server: McpServer): void {
499
499
  server.registerTool('gog_docs_find_range', {
500
500
  description: 'Map literal text in a Google Doc to its Docs API UTF-16 index range(s). Read-only helper for computing the start/end indices that index-based tools (gog_docs_delete, gog_docs_update --replace-range) need. Returns the first match by default; use occurrence to pick a specific one or all to return every match.',
501
501
  annotations: { readOnlyHint: true },
502
- inputSchema: {
502
+ inputSchema: z.object({
503
503
  docId: z.string().describe('Doc ID (from the URL)'),
504
504
  text: z.string().describe('Literal text to locate'),
505
505
  occurrence: z.number().int().optional().describe('Return the Nth occurrence (1-based; default: first)'),
@@ -510,7 +510,7 @@ export function registerExtraDocsTools(server: McpServer): void {
510
510
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
511
511
  segment: z.string().optional().describe('Target an exact header, footer, or footnote segment ID (header/footer IDs come from gog_docs_header_list / gog_docs_footer_list; footnote segment IDs appear in gog_docs_read json mode) instead of the document body.'),
512
512
  account: accountParam,
513
- },
513
+ }),
514
514
  }, async ({ docId, text, occurrence, matchCase, normalizeWhitespace, all, failEmpty, tab, segment, account }) => {
515
515
  const args = ['docs', 'find-range', docId, text];
516
516
  if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
@@ -528,11 +528,11 @@ export function registerExtraDocsTools(server: McpServer): void {
528
528
  // end to submit them atomically against the locked revision.
529
529
  server.registerTool('gog_batch_begin', {
530
530
  description: 'Open a persisted, revision-locked request batch for a Google Doc. Subsequent docs mutation tools called with batch=<batchId> append their requests locally instead of applying them; gog_batch_end submits everything atomically. Returns the batchId.',
531
- inputSchema: {
531
+ inputSchema: z.object({
532
532
  docId: z.string().describe('Google Doc ID the batch is locked to'),
533
533
  name: z.string().optional().describe('Optional batch label'),
534
534
  account: accountParam,
535
- },
535
+ }),
536
536
  }, async ({ docId, name, account }) => {
537
537
  const args = ['batch', 'begin', `--doc=${docId}`];
538
538
  if (name) args.push(`--name=${name}`);
@@ -542,12 +542,12 @@ export function registerExtraDocsTools(server: McpServer): void {
542
542
  server.registerTool('gog_batch_end', {
543
543
  description: 'Submit a persisted batch: applies every composed request to the doc in one atomic batchUpdate against the locked revision. Atomic by default — set autoSplit to submit >500-request batches as ordered chunks (non-atomic), or continueOnError to retry individually after an atomic validation failure, retaining failures in the batch.',
544
544
  annotations: { destructiveHint: true },
545
- inputSchema: {
545
+ inputSchema: z.object({
546
546
  batchId: z.string().describe('Batch ID (from gog_batch_begin)'),
547
547
  autoSplit: z.boolean().optional().describe('Submit batches over 500 requests as ordered chunks (non-atomic)'),
548
548
  continueOnError: z.boolean().optional().describe('After an atomic validation failure, submit requests individually and retain failures'),
549
549
  account: accountParam,
550
- },
550
+ }),
551
551
  }, async ({ batchId, autoSplit, continueOnError, account }) => {
552
552
  const args = ['batch', 'end', batchId];
553
553
  if (autoSplit) args.push('--auto-split');
@@ -558,10 +558,10 @@ export function registerExtraDocsTools(server: McpServer): void {
558
558
  server.registerTool('gog_batch_abort', {
559
559
  description: 'Discard a persisted batch and its composed requests without applying anything to the doc.',
560
560
  annotations: { destructiveHint: true },
561
- inputSchema: {
561
+ inputSchema: z.object({
562
562
  batchId: z.string().describe('Batch ID to discard'),
563
563
  account: accountParam,
564
- },
564
+ }),
565
565
  }, async ({ batchId, account }) => {
566
566
  return runOrDiagnose(['batch', 'abort', batchId], { account });
567
567
  });
@@ -569,9 +569,9 @@ export function registerExtraDocsTools(server: McpServer): void {
569
569
  server.registerTool('gog_batch_list', {
570
570
  description: 'List persisted Docs request batches (id, doc, label, request count, status).',
571
571
  annotations: { readOnlyHint: true },
572
- inputSchema: {
572
+ inputSchema: z.object({
573
573
  account: accountParam,
574
- },
574
+ }),
575
575
  }, async ({ account }) => {
576
576
  return runOrDiagnose(['batch', 'list'], { account });
577
577
  });
@@ -579,10 +579,10 @@ export function registerExtraDocsTools(server: McpServer): void {
579
579
  server.registerTool('gog_batch_show', {
580
580
  description: 'Show one persisted batch: its doc, locked revision, and the composed requests awaiting submission.',
581
581
  annotations: { readOnlyHint: true },
582
- inputSchema: {
582
+ inputSchema: z.object({
583
583
  batchId: z.string().describe('Batch ID'),
584
584
  account: accountParam,
585
- },
585
+ }),
586
586
  }, async ({ batchId, account }) => {
587
587
  return runOrDiagnose(['batch', 'show', batchId], { account });
588
588
  });
@@ -590,10 +590,10 @@ export function registerExtraDocsTools(server: McpServer): void {
590
590
  server.registerTool('gog_batch_prune', {
591
591
  description: 'Delete stale persisted batches (not updated within olderThan).',
592
592
  annotations: { destructiveHint: true },
593
- inputSchema: {
593
+ inputSchema: z.object({
594
594
  olderThan: z.string().optional().describe('Delete batches not updated within this duration (e.g. 72h, 7d)'),
595
595
  account: accountParam,
596
- },
596
+ }),
597
597
  }, async ({ olderThan, account }) => {
598
598
  const args = ['batch', 'prune'];
599
599
  if (olderThan) args.push(`--older-than=${olderThan}`);
@@ -603,7 +603,7 @@ export function registerExtraDocsTools(server: McpServer): void {
603
603
  server.registerTool('gog_docs_table_column_width', {
604
604
  description: 'Set a fixed width (in points) for a table column, or reset columns to Docs-managed even distribution. Target the table by 1-based index in document order (negative counts from the end) and the column by 1-based number. Pass evenlyDistributed without col to reset every column in the table.',
605
605
  annotations: { destructiveHint: true },
606
- inputSchema: {
606
+ inputSchema: z.object({
607
607
  docId: z.string().describe('Doc ID (from the URL)'),
608
608
  col: z.number().int().optional().describe('1-based column number. Omit with evenlyDistributed to reset all columns.'),
609
609
  width: z.number().optional().describe('Fixed column width in points (minimum 5pt). Mutually exclusive with evenlyDistributed.'),
@@ -612,7 +612,7 @@ export function registerExtraDocsTools(server: McpServer): void {
612
612
  tab: z.string().optional().describe('Target tab title or ID'),
613
613
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
614
614
  account: accountParam,
615
- },
615
+ }),
616
616
  }, async ({ docId, col, width, evenlyDistributed, tableIndex, tab, batch, account }) => {
617
617
  const args = ['docs', 'table-column-width', docId];
618
618
  if (col !== undefined) args.push(`--col=${col}`);
@@ -630,14 +630,14 @@ export function registerExtraDocsTools(server: McpServer): void {
630
630
 
631
631
  server.registerTool('gog_docs_table_row_insert', {
632
632
  description: 'Insert a row into a native Google Docs table, optionally populated from a JSON string array. Inserts before the 1-based position given by `at` (negative counts from the end; "end" appends — the default).',
633
- inputSchema: {
633
+ inputSchema: z.object({
634
634
  docId: z.string().describe('Doc ID (from the URL)'),
635
635
  table: tableSelectorParam,
636
636
  at: z.string().optional().describe('Insert before this 1-based row, a negative index from the end, or "end" to append (default: end)'),
637
637
  valuesJson: z.string().optional().describe('JSON string array with the new row\'s cell values, e.g. ["Name","Qty"]'),
638
638
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
639
639
  account: accountParam,
640
- },
640
+ }),
641
641
  }, async ({ docId, table, at, valuesJson, tab, account }) => {
642
642
  const args = ['docs', 'table-row', 'insert', docId];
643
643
  if (table) args.push(`--table=${table}`);
@@ -650,13 +650,13 @@ export function registerExtraDocsTools(server: McpServer): void {
650
650
  server.registerTool('gog_docs_table_row_delete', {
651
651
  description: 'Delete a row from a native Google Docs table by 1-based row number (negative counts from the end). The row\'s cell content is lost.',
652
652
  annotations: { destructiveHint: true },
653
- inputSchema: {
653
+ inputSchema: z.object({
654
654
  docId: z.string().describe('Doc ID (from the URL)'),
655
655
  row: z.number().int().describe('1-based row number; negative indexes count from the end'),
656
656
  table: tableSelectorParam,
657
657
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
658
658
  account: accountParam,
659
- },
659
+ }),
660
660
  }, async ({ docId, row, table, tab, account }) => {
661
661
  const args = ['docs', 'table-row', 'delete', docId, `--row=${row}`];
662
662
  if (table) args.push(`--table=${table}`);
@@ -666,13 +666,13 @@ export function registerExtraDocsTools(server: McpServer): void {
666
666
 
667
667
  server.registerTool('gog_docs_table_column_insert', {
668
668
  description: 'Insert a column into a native Google Docs table. Inserts before the 1-based position given by `at` (negative counts from the end; "end" appends — the default).',
669
- inputSchema: {
669
+ inputSchema: z.object({
670
670
  docId: z.string().describe('Doc ID (from the URL)'),
671
671
  table: tableSelectorParam,
672
672
  at: z.string().optional().describe('Insert before this 1-based column, a negative index from the end, or "end" to append (default: end)'),
673
673
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
674
674
  account: accountParam,
675
- },
675
+ }),
676
676
  }, async ({ docId, table, at, tab, account }) => {
677
677
  const args = ['docs', 'table-column', 'insert', docId];
678
678
  if (at) args.push(`--at=${at}`);
@@ -684,13 +684,13 @@ export function registerExtraDocsTools(server: McpServer): void {
684
684
  server.registerTool('gog_docs_table_column_delete', {
685
685
  description: 'Delete a column from a native Google Docs table by 1-based column number (negative counts from the end). The column\'s cell content is lost.',
686
686
  annotations: { destructiveHint: true },
687
- inputSchema: {
687
+ inputSchema: z.object({
688
688
  docId: z.string().describe('Doc ID (from the URL)'),
689
689
  col: z.number().int().describe('1-based column number; negative indexes count from the end'),
690
690
  table: tableSelectorParam,
691
691
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
692
692
  account: accountParam,
693
- },
693
+ }),
694
694
  }, async ({ docId, col, table, tab, account }) => {
695
695
  const args = ['docs', 'table-column', 'delete', docId, `--col=${col}`];
696
696
  if (table) args.push(`--table=${table}`);
@@ -701,13 +701,13 @@ export function registerExtraDocsTools(server: McpServer): void {
701
701
  server.registerTool('gog_docs_table_merge', {
702
702
  description: 'Merge a rectangular cell range in a native Google Docs table. Content of non-first cells in the range is absorbed/discarded by the merge — use gog_docs_table_unmerge to split back.',
703
703
  annotations: { destructiveHint: true },
704
- inputSchema: {
704
+ inputSchema: z.object({
705
705
  docId: z.string().describe('Doc ID (from the URL)'),
706
706
  range: z.string().describe('1-based cell range r1,c1:r2,c2 (e.g. "1,1:2,3")'),
707
707
  table: tableSelectorParam,
708
708
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
709
709
  account: accountParam,
710
- },
710
+ }),
711
711
  }, async ({ docId, range, table, tab, account }) => {
712
712
  const args = ['docs', 'table-merge', docId, `--range=${range}`];
713
713
  if (table) args.push(`--table=${table}`);
@@ -717,13 +717,13 @@ export function registerExtraDocsTools(server: McpServer): void {
717
717
 
718
718
  server.registerTool('gog_docs_table_unmerge', {
719
719
  description: 'Unmerge (split) the merged region containing a given cell in a native Google Docs table.',
720
- inputSchema: {
720
+ inputSchema: z.object({
721
721
  docId: z.string().describe('Doc ID (from the URL)'),
722
722
  cell: z.string().describe('1-based cell r,c inside the merged region (e.g. "1,1")'),
723
723
  table: tableSelectorParam,
724
724
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
725
725
  account: accountParam,
726
- },
726
+ }),
727
727
  }, async ({ docId, cell, table, tab, account }) => {
728
728
  const args = ['docs', 'table-unmerge', docId, `--cell=${cell}`];
729
729
  if (table) args.push(`--table=${table}`);
@@ -733,7 +733,7 @@ export function registerExtraDocsTools(server: McpServer): void {
733
733
 
734
734
  server.registerTool('gog_docs_named_range_create', {
735
735
  description: 'Create a named range — a durable, tab-aware anchor over a span of document text that survives subsequent edits (unlike raw indices). Anchor by literal text (`at`) or explicit UTF-16 start/end indices. Pair with gog_docs_named_range_replace for repeatable templated updates.',
736
- inputSchema: {
736
+ inputSchema: z.object({
737
737
  docId: z.string().describe('Doc ID (from the URL)'),
738
738
  name: z.string().describe('Unique named range name'),
739
739
  at: z.string().optional().describe('Create the range around this literal matched text, instead of start/end indices'),
@@ -743,7 +743,7 @@ export function registerExtraDocsTools(server: McpServer): void {
743
743
  end: z.number().int().optional().describe('Range end UTF-16 index (exclusive). Required with start unless `at` is set.'),
744
744
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
745
745
  account: accountParam,
746
- },
746
+ }),
747
747
  }, async ({ docId, name, at, occurrence, matchCase, start, end, tab, account }) => {
748
748
  const args = ['docs', 'named-range', 'create', docId, `--name=${name}`];
749
749
  if (at) args.push(`--at=${at}`);
@@ -758,12 +758,12 @@ export function registerExtraDocsTools(server: McpServer): void {
758
758
  server.registerTool('gog_docs_named_range_list', {
759
759
  description: 'List named ranges in a Google Doc, optionally filtered by name.',
760
760
  annotations: { readOnlyHint: true },
761
- inputSchema: {
761
+ inputSchema: z.object({
762
762
  docId: z.string().describe('Doc ID (from the URL)'),
763
763
  name: z.string().optional().describe('Only return ranges with this name'),
764
764
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
765
765
  account: accountParam,
766
- },
766
+ }),
767
767
  }, async ({ docId, name, tab, account }) => {
768
768
  const args = ['docs', 'named-range', 'list', docId];
769
769
  if (name) args.push(`--name=${name}`);
@@ -774,12 +774,12 @@ export function registerExtraDocsTools(server: McpServer): void {
774
774
  server.registerTool('gog_docs_named_range_delete', {
775
775
  description: 'Delete a named range (the anchor only — the underlying document text is untouched).',
776
776
  annotations: { destructiveHint: true },
777
- inputSchema: {
777
+ inputSchema: z.object({
778
778
  docId: z.string().describe('Doc ID (from the URL)'),
779
779
  nameOrId: z.string().describe('Named range name or ID'),
780
780
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
781
781
  account: accountParam,
782
- },
782
+ }),
783
783
  }, async ({ docId, nameOrId, tab, account }) => {
784
784
  const args = ['docs', 'named-range', 'delete', docId, nameOrId];
785
785
  if (tab) args.push(`--tab=${tab}`);
@@ -789,14 +789,14 @@ export function registerExtraDocsTools(server: McpServer): void {
789
789
  server.registerTool('gog_docs_named_range_replace', {
790
790
  description: 'Replace the text inside a named range with new content (inline or from a file), keeping the anchor for future updates — the templated-update workhorse.',
791
791
  annotations: { destructiveHint: true },
792
- inputSchema: {
792
+ inputSchema: z.object({
793
793
  docId: z.string().describe('Doc ID (from the URL)'),
794
794
  nameOrId: z.string().describe('Named range name or ID'),
795
795
  text: z.string().optional().describe('Replacement text'),
796
796
  file: z.string().optional().describe('Path to a file with the replacement text'),
797
797
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
798
798
  account: accountParam,
799
- },
799
+ }),
800
800
  }, async ({ docId, nameOrId, text, file, tab, account }) => {
801
801
  const args = ['docs', 'named-range', 'replace', docId, nameOrId];
802
802
  if (text !== undefined) args.push(`--text=${text}`);
@@ -808,11 +808,11 @@ export function registerExtraDocsTools(server: McpServer): void {
808
808
  server.registerTool('gog_docs_tables_list', {
809
809
  description: 'Enumerate the native tables in a Google Doc (dimensions, position) — the index/first-cell-text it reports feeds the `table` selector on the table-row/column/merge tools.',
810
810
  annotations: { readOnlyHint: true },
811
- inputSchema: {
811
+ inputSchema: z.object({
812
812
  docId: z.string().describe('Doc ID (from the URL)'),
813
813
  tab: z.string().optional().describe('Tab title or ID (omit for default)'),
814
814
  account: accountParam,
815
- },
815
+ }),
816
816
  }, async ({ docId, tab, account }) => {
817
817
  const args = ['docs', 'tables', 'list', docId];
818
818
  if (tab) args.push(`--tab=${tab}`);
@@ -822,11 +822,11 @@ export function registerExtraDocsTools(server: McpServer): void {
822
822
  server.registerTool('gog_docs_images_list', {
823
823
  description: 'Enumerate inline/positioned images in a Google Doc.',
824
824
  annotations: { readOnlyHint: true },
825
- inputSchema: {
825
+ inputSchema: z.object({
826
826
  docId: z.string().describe('Doc ID (from the URL)'),
827
827
  tab: z.string().optional().describe('Tab title or ID (omit for default)'),
828
828
  account: accountParam,
829
- },
829
+ }),
830
830
  }, async ({ docId, tab, account }) => {
831
831
  const args = ['docs', 'images', 'list', docId];
832
832
  if (tab) args.push(`--tab=${tab}`);
@@ -836,12 +836,12 @@ export function registerExtraDocsTools(server: McpServer): void {
836
836
  server.registerTool('gog_docs_headings_list', {
837
837
  description: 'Enumerate headings in a Google Doc (a lightweight outline view), optionally filtered to one heading level.',
838
838
  annotations: { readOnlyHint: true },
839
- inputSchema: {
839
+ inputSchema: z.object({
840
840
  docId: z.string().describe('Doc ID (from the URL)'),
841
841
  level: z.number().int().optional().describe('Only return this heading level (1-6)'),
842
842
  tab: z.string().optional().describe('Tab title or ID (omit for default)'),
843
843
  account: accountParam,
844
- },
844
+ }),
845
845
  }, async ({ docId, level, tab, account }) => {
846
846
  const args = ['docs', 'headings', 'list', docId];
847
847
  if (level !== undefined) args.push(`--level=${level}`);
@@ -852,12 +852,12 @@ export function registerExtraDocsTools(server: McpServer): void {
852
852
  server.registerTool('gog_docs_paragraphs_list', {
853
853
  description: 'Enumerate paragraphs in a Google Doc with emptiness, text-run ranges, styles, and links — richer than gog_docs_structure when you need per-run detail. Optionally filter to one named style.',
854
854
  annotations: { readOnlyHint: true },
855
- inputSchema: {
855
+ inputSchema: z.object({
856
856
  docId: z.string().describe('Doc ID (from the URL)'),
857
857
  style: z.string().optional().describe('Only return this named style (e.g. NORMAL_TEXT or HEADING_2)'),
858
858
  tab: z.string().optional().describe('Tab title or ID (omit for default)'),
859
859
  account: accountParam,
860
- },
860
+ }),
861
861
  }, async ({ docId, style, tab, account }) => {
862
862
  const args = ['docs', 'paragraphs', 'list', docId];
863
863
  if (style) args.push(`--style=${style}`);
@@ -868,7 +868,7 @@ export function registerExtraDocsTools(server: McpServer): void {
868
868
  server.registerTool('gog_docs_insert_page_break', {
869
869
  description: 'Insert a Google Docs page break via InsertPageBreakRequest — the only path for multi-page deliverables (markdown has no page-break construct). Specify `index` for a precise character position, or `atEnd` for end-of-doc.',
870
870
  annotations: { destructiveHint: true },
871
- inputSchema: {
871
+ inputSchema: z.object({
872
872
  docId: z.string().describe('Doc ID (from the URL)'),
873
873
  index: z.number().int().optional().describe('Character index to insert at (1 = beginning). Omit or use atEnd for end-of-doc.'),
874
874
  atEnd: z.boolean().optional().describe('Insert at end-of-doc/tab (mutually exclusive with index)'),
@@ -878,7 +878,7 @@ export function registerExtraDocsTools(server: McpServer): void {
878
878
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
879
879
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
880
880
  account: accountParam,
881
- },
881
+ }),
882
882
  }, async ({ docId, index, atEnd, at, occurrence, matchCase, tab, batch, account }) => {
883
883
  const args = ['docs', 'insert-page-break', docId];
884
884
  if (index !== undefined) args.push(`--index=${index}`);
@@ -894,7 +894,7 @@ export function registerExtraDocsTools(server: McpServer): void {
894
894
  server.registerTool('gog_docs_page_layout', {
895
895
  description: 'Toggle the page layout (pageless | pages) of an existing Google Doc. Sibling to the --pageless flag on docs create/write/update for docs that were already created (e.g. by Drive markdown conversion) without the desired layout.',
896
896
  annotations: { destructiveHint: true },
897
- inputSchema: {
897
+ inputSchema: z.object({
898
898
  docId: z.string().describe('Doc ID (from the URL)'),
899
899
  layout: z.enum(['pageless', 'pages']).optional().describe('Page layout (default: pageless)'),
900
900
  pageSize: z.enum(['A3', 'A4', 'A5', 'Letter', 'Legal', 'Tabloid']).optional().describe('Named page size preset (only applies in paged layout)'),
@@ -905,7 +905,7 @@ export function registerExtraDocsTools(server: McpServer): void {
905
905
  marginLeft: z.string().optional().describe('Left page margin (points by default; supports pt, in, cm, mm)'),
906
906
  marginRight: z.string().optional().describe('Right page margin (points by default; supports pt, in, cm, mm)'),
907
907
  account: accountParam,
908
- },
908
+ }),
909
909
  }, async ({ docId, layout, pageSize, pageWidth, pageHeight, marginTop, marginBottom, marginLeft, marginRight, account }) => {
910
910
  const args = ['docs', 'page-layout', docId];
911
911
  if (layout) args.push(`--layout=${layout}`);
@@ -922,7 +922,7 @@ export function registerExtraDocsTools(server: McpServer): void {
922
922
  server.registerTool('gog_docs_insert_table', {
923
923
  description: 'Insert a native Google Docs table via InsertTableRequest, bypassing the markdown writer. Use this instead of writing a markdown table when you need precise dimensions or to avoid the markdown writer\'s table limitations. `valuesJson` is a JSON 2D string array whose dimensions must match rows x cols.',
924
924
  annotations: { destructiveHint: true },
925
- inputSchema: {
925
+ inputSchema: z.object({
926
926
  docId: z.string().describe('Doc ID (from the URL)'),
927
927
  rows: z.number().int().min(1).describe('Number of rows (>=1)'),
928
928
  cols: z.number().int().min(1).describe('Number of columns (>=1)'),
@@ -931,7 +931,7 @@ export function registerExtraDocsTools(server: McpServer): void {
931
931
  valuesJson: z.string().optional().describe('Cell values as a JSON 2D string array; dimensions must match rows x cols when supplied'),
932
932
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
933
933
  account: accountParam,
934
- },
934
+ }),
935
935
  }, async ({ docId, rows, cols, index, atEnd, valuesJson, tab, account }) => {
936
936
  const args = ['docs', 'insert-table', docId, `--rows=${rows}`, `--cols=${cols}`];
937
937
  if (index !== undefined) args.push(`--index=${index}`);
@@ -944,7 +944,7 @@ export function registerExtraDocsTools(server: McpServer): void {
944
944
  server.registerTool('gog_docs_cell_update', {
945
945
  description: 'Replace (or append to) the content of a single table cell, addressed by table / row / column — non-destructive to the rest of the table, unlike index-based edits that shift on every change. Provide content inline or read it from contentFile. Note: row/col/tableIndex are 1-based here; the sibling gog_docs_cell_style uses 0-based addressing.',
946
946
  annotations: { destructiveHint: true },
947
- inputSchema: {
947
+ inputSchema: z.object({
948
948
  docId: z.string().describe('Doc ID (from the URL)'),
949
949
  row: z.number().int().describe('1-based row number'),
950
950
  col: z.number().int().describe('1-based column number'),
@@ -955,7 +955,7 @@ export function registerExtraDocsTools(server: McpServer): void {
955
955
  tableIndex: z.number().int().optional().describe('1-based table index in document order; negative counts from the end (default: 1)'),
956
956
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
957
957
  account: accountParam,
958
- },
958
+ }),
959
959
  }, async ({ docId, row, col, content, contentFile, append, format, tableIndex, tab, account }) => {
960
960
  // gog HARD-ERRORS on both forms ("cannot use both --content and
961
961
  // --content-file"), so reject here with a clearer message before gog runs.
@@ -983,7 +983,7 @@ export function registerExtraDocsTools(server: McpServer): void {
983
983
  server.registerTool('gog_docs_cell_style', {
984
984
  description: 'Apply background color and/or inline text styling (bold, italic, underline, colors) to one or more table cells, addressed by 0-based row/column with optional spans. Sibling to gog_docs_cell_update, which changes cell content rather than styling. Note: row/col/tableIndex are 0-based here; gog_docs_cell_update uses 1-based addressing.',
985
985
  annotations: { destructiveHint: true },
986
- inputSchema: {
986
+ inputSchema: z.object({
987
987
  docId: z.string().describe('Doc ID (from the URL)'),
988
988
  row: z.number().int().describe('0-based row number'),
989
989
  col: z.number().int().describe('0-based column number'),
@@ -1009,7 +1009,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1009
1009
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1010
1010
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
1011
1011
  account: accountParam,
1012
- },
1012
+ }),
1013
1013
  }, async ({ docId, row, col, rowSpan, colSpan, backgroundColor, textColor, bold, italic, underline, borderAll, borderTop, borderBottom, borderLeft, borderRight, paddingAll, paddingTop, paddingBottom, paddingLeft, paddingRight, contentAlign, tableIndex, tab, batch, account }) => {
1014
1014
  const args = ['docs', 'cell-style', docId, `--row=${row}`, `--col=${col}`];
1015
1015
  if (rowSpan !== undefined) args.push(`--row-span=${rowSpan}`);
@@ -1039,7 +1039,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1039
1039
  server.registerTool('gog_docs_insert_image', {
1040
1040
  description: 'Insert an image into a Google Doc from a local file or a public HTTPS URL. file: uploaded to Drive, temporarily shared so Docs can fetch it, inserted, then the public permission is revoked. url: inserted directly with no Drive upload or temporary sharing. Replaces placeholder text (at) or appends at end-of-doc.',
1041
1041
  annotations: { destructiveHint: true },
1042
- inputSchema: {
1042
+ inputSchema: z.object({
1043
1043
  docId: z.string().describe('Doc ID (from the URL)'),
1044
1044
  file: z.string().optional().describe('Local PNG, JPEG, or GIF image to upload and insert (exactly one of file or url)'),
1045
1045
  url: z.string().optional().describe('Public HTTPS image URL to insert directly — no Drive upload or temporary public sharing (exactly one of file or url)'),
@@ -1053,7 +1053,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1053
1053
  onRestricted: z.enum(['error', 'link']).optional().describe('If public sharing is blocked: error out, or fall back to a linked image (default: error)'),
1054
1054
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1055
1055
  account: accountParam,
1056
- },
1056
+ }),
1057
1057
  }, async ({ docId, file, url, at, before, after, width, height, name, parent, onRestricted, tab, account }) => {
1058
1058
  const args = ['docs', 'insert-image', docId];
1059
1059
  if (file) args.push(`--file=${file}`);
@@ -1074,7 +1074,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1074
1074
  server.registerTool('gog_docs_insert_person', {
1075
1075
  description: 'Insert a native Google Docs person smart chip (the interactive @-mention chip) for an email address, at a character index or end-of-doc.',
1076
1076
  annotations: { destructiveHint: true },
1077
- inputSchema: {
1077
+ inputSchema: z.object({
1078
1078
  docId: z.string().describe('Doc ID (from the URL)'),
1079
1079
  email: z.string().describe('Email address for the person chip'),
1080
1080
  index: z.number().int().optional().describe('Character index to insert at. Omit or use atEnd for end-of-doc.'),
@@ -1085,7 +1085,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1085
1085
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1086
1086
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
1087
1087
  account: accountParam,
1088
- },
1088
+ }),
1089
1089
  }, async ({ docId, email, index, atEnd, at, occurrence, matchCase, tab, batch, account }) => {
1090
1090
  const args = ['docs', 'insert-person', docId, `--email=${email}`];
1091
1091
  if (index !== undefined) args.push(`--index=${index}`);
@@ -1101,7 +1101,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1101
1101
  server.registerTool('gog_docs_insert_date_chip', {
1102
1102
  description: 'Insert a native Google Docs date smart chip, at a character index or end-of-doc.',
1103
1103
  annotations: { destructiveHint: true },
1104
- inputSchema: {
1104
+ inputSchema: z.object({
1105
1105
  docId: z.string().describe('Doc ID (from the URL)'),
1106
1106
  date: z.string().optional().describe('Date to insert as YYYY-MM-DD (default: today)'),
1107
1107
  format: z.enum(['abbreviated', 'full', 'iso']).optional().describe('Date display format (default: abbreviated)'),
@@ -1110,7 +1110,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1110
1110
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1111
1111
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
1112
1112
  account: accountParam,
1113
- },
1113
+ }),
1114
1114
  }, async ({ docId, date, format, index, atEnd, tab, batch, account }) => {
1115
1115
  const args = ['docs', 'insert-date-chip', docId];
1116
1116
  if (date) args.push(`--date=${date}`);
@@ -1124,14 +1124,14 @@ export function registerExtraDocsTools(server: McpServer): void {
1124
1124
 
1125
1125
  server.registerTool('gog_docs_add_tab', {
1126
1126
  description: 'Add a tab to a Google Doc. Tabs partition a doc into independently-addressable sections (multi-tab docs). Optionally set the title, zero-based position, parent tab (for nesting), and an emoji icon.',
1127
- inputSchema: {
1127
+ inputSchema: z.object({
1128
1128
  docId: z.string().describe('Doc ID (from the URL)'),
1129
1129
  title: z.string().optional().describe('User-visible tab title'),
1130
1130
  index: z.number().int().optional().describe('Zero-based tab position within the parent'),
1131
1131
  parentTab: z.string().optional().describe('Parent tab title or ID to nest this tab under'),
1132
1132
  iconEmoji: z.string().optional().describe('Emoji icon for the tab'),
1133
1133
  account: accountParam,
1134
- },
1134
+ }),
1135
1135
  }, async ({ docId, title, index, parentTab, iconEmoji, account }) => {
1136
1136
  const args = ['docs', 'add-tab', docId];
1137
1137
  if (title) args.push(`--title=${title}`);
@@ -1143,12 +1143,12 @@ export function registerExtraDocsTools(server: McpServer): void {
1143
1143
 
1144
1144
  server.registerTool('gog_docs_rename_tab', {
1145
1145
  description: 'Rename a tab in a Google Doc. Identify the existing tab by title or ID and give it a new title.',
1146
- inputSchema: {
1146
+ inputSchema: z.object({
1147
1147
  docId: z.string().describe('Doc ID (from the URL)'),
1148
1148
  tab: z.string().describe('Existing tab title or ID to rename'),
1149
1149
  title: z.string().describe('New user-visible tab title'),
1150
1150
  account: accountParam,
1151
- },
1151
+ }),
1152
1152
  }, async ({ docId, tab, title, account }) => {
1153
1153
  const args = ['docs', 'rename-tab', docId, `--tab=${tab}`, `--title=${title}`];
1154
1154
  return runOrDiagnose(args, { account });
@@ -1157,11 +1157,11 @@ export function registerExtraDocsTools(server: McpServer): void {
1157
1157
  server.registerTool('gog_docs_delete_tab', {
1158
1158
  description: 'Delete a tab (and its content) from a Google Doc. Identify the tab by title or ID. This permanently removes the tab and everything in it.',
1159
1159
  annotations: { destructiveHint: true },
1160
- inputSchema: {
1160
+ inputSchema: z.object({
1161
1161
  docId: z.string().describe('Doc ID (from the URL)'),
1162
1162
  tab: z.string().describe('Existing tab title or ID to delete'),
1163
1163
  account: accountParam,
1164
- },
1164
+ }),
1165
1165
  }, async ({ docId, tab, account }) => {
1166
1166
  const args = ['docs', 'delete-tab', docId, `--tab=${tab}`, '--force']; // gog gates this op; without --force the runner's --no-input makes it refuse
1167
1167
  return runOrDiagnose(args, { account });
@@ -1170,10 +1170,10 @@ export function registerExtraDocsTools(server: McpServer): void {
1170
1170
  server.registerTool('gog_docs_clear', {
1171
1171
  description: 'Clear all content from a Google Doc, leaving an empty document. The doc itself is preserved (same ID/URL); only its body content is removed. To delete the whole doc instead, use gog_docs_trash.',
1172
1172
  annotations: { destructiveHint: true },
1173
- inputSchema: {
1173
+ inputSchema: z.object({
1174
1174
  docId: z.string().describe('Doc ID (from the URL)'),
1175
1175
  account: accountParam,
1176
- },
1176
+ }),
1177
1177
  }, async ({ docId, account }) => {
1178
1178
  return runOrDiagnose(['docs', 'clear', docId], { account });
1179
1179
  });
@@ -1183,7 +1183,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1183
1183
  server.registerTool('gog_docs_insert_footnote', {
1184
1184
  description: 'Insert a footnote at a character index (or end-of-doc) and populate its text. The footnote reference mark is placed in the body; the footnote content goes in the new footnote segment.',
1185
1185
  annotations: { destructiveHint: true },
1186
- inputSchema: {
1186
+ inputSchema: z.object({
1187
1187
  docId: z.string().describe('Doc ID (from the URL)'),
1188
1188
  text: z.string().optional().describe('Footnote text'),
1189
1189
  file: z.string().optional().describe('Read footnote text from a file on the gog server. gog also accepts "-" for stdin, but this server never writes to gog\'s stdin, so "-" would hang until the call times out.'),
@@ -1194,7 +1194,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1194
1194
  matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
1195
1195
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1196
1196
  account: accountParam,
1197
- },
1197
+ }),
1198
1198
  }, async ({ docId, text, file, index, atEnd, at, occurrence, matchCase, tab, account }) => {
1199
1199
  const args = ['docs', 'insert-footnote', docId];
1200
1200
  if (text !== undefined) args.push(`--text=${text}`);
@@ -1211,7 +1211,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1211
1211
  server.registerTool('gog_docs_insert_section_break', {
1212
1212
  description: 'Insert a continuous or next-page section break at a character index (or end-of-doc). Section breaks enable per-section layout such as column counts (see gog_docs_section_columns).',
1213
1213
  annotations: { destructiveHint: true },
1214
- inputSchema: {
1214
+ inputSchema: z.object({
1215
1215
  docId: z.string().describe('Doc ID (from the URL)'),
1216
1216
  type: z.enum(['next-page', 'continuous']).optional().describe('Section break type (default: next-page)'),
1217
1217
  index: z.number().int().optional().describe('Character index to insert at (1 = beginning). Omit or use atEnd for end-of-doc.'),
@@ -1222,7 +1222,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1222
1222
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1223
1223
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
1224
1224
  account: accountParam,
1225
- },
1225
+ }),
1226
1226
  }, async ({ docId, type, index, atEnd, at, occurrence, matchCase, tab, batch, account }) => {
1227
1227
  const args = ['docs', 'insert-section-break', docId];
1228
1228
  if (type) args.push(`--type=${type}`);
@@ -1239,7 +1239,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1239
1239
  server.registerTool('gog_docs_insert_horizontal_rule', {
1240
1240
  description: 'Insert a horizontal rule (paragraph border) at a character index (or end-of-doc).',
1241
1241
  annotations: { destructiveHint: true },
1242
- inputSchema: {
1242
+ inputSchema: z.object({
1243
1243
  docId: z.string().describe('Doc ID (from the URL)'),
1244
1244
  index: z.number().int().optional().describe('Character index to insert at (1 = beginning). Omit or use atEnd for end-of-doc.'),
1245
1245
  atEnd: z.boolean().optional().describe('Target end-of-doc/tab (mutually exclusive with index and at)'),
@@ -1249,7 +1249,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1249
1249
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1250
1250
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
1251
1251
  account: accountParam,
1252
- },
1252
+ }),
1253
1253
  }, async ({ docId, index, atEnd, at, occurrence, matchCase, tab, batch, account }) => {
1254
1254
  const args = ['docs', 'insert-horizontal-rule', docId];
1255
1255
  if (index !== undefined) args.push(`--index=${index}`);
@@ -1265,7 +1265,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1265
1265
  server.registerTool('gog_docs_section_columns', {
1266
1266
  description: 'Set the column count (1-3) for the document section containing the target position. Use count=1 to reset to a single column. Anchor by index, end-of-doc, or literal text.',
1267
1267
  annotations: { destructiveHint: true },
1268
- inputSchema: {
1268
+ inputSchema: z.object({
1269
1269
  docId: z.string().describe('Doc ID (from the URL)'),
1270
1270
  count: z.number().int().min(1).max(3).describe('Number of columns (1-3; 1 resets to one column)'),
1271
1271
  separator: z.enum(['none', 'between']).optional().describe('Column separator line: none or between'),
@@ -1277,7 +1277,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1277
1277
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1278
1278
  batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
1279
1279
  account: accountParam,
1280
- },
1280
+ }),
1281
1281
  }, async ({ docId, count, separator, index, atEnd, at, occurrence, matchCase, tab, batch, account }) => {
1282
1282
  const args = ['docs', 'section-columns', docId, `--count=${count}`];
1283
1283
  if (separator) args.push(`--separator=${separator}`);
@@ -1297,11 +1297,11 @@ export function registerExtraDocsTools(server: McpServer): void {
1297
1297
  server.registerTool('gog_docs_header_list', {
1298
1298
  description: 'List a Google Doc\'s headers and their segment IDs. Use a segment ID with the segment param on gog_docs_insert / gog_docs_update / gog_docs_delete / gog_docs_format / gog_docs_find_range to edit inside a header.',
1299
1299
  annotations: { readOnlyHint: true },
1300
- inputSchema: {
1300
+ inputSchema: z.object({
1301
1301
  docId: z.string().describe('Doc ID (from the URL)'),
1302
1302
  tab: z.string().optional().describe('Limit results to a tab title or ID'),
1303
1303
  account: accountParam,
1304
- },
1304
+ }),
1305
1305
  }, async ({ docId, tab, account }) => {
1306
1306
  const args = ['docs', 'header', 'list', docId];
1307
1307
  if (tab) args.push(`--tab=${tab}`);
@@ -1310,7 +1310,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1310
1310
 
1311
1311
  server.registerTool('gog_docs_header_create', {
1312
1312
  description: 'Create a header in a Google Doc and optionally populate its initial text. Returns the new header segment ID.',
1313
- inputSchema: {
1313
+ inputSchema: z.object({
1314
1314
  docId: z.string().describe('Doc ID (from the URL)'),
1315
1315
  text: z.string().optional().describe('Initial header text'),
1316
1316
  file: z.string().optional().describe('Read initial header text from a file on the gog server. gog also accepts "-" for stdin, but this server never writes to gog\'s stdin, so "-" would hang until the call times out.'),
@@ -1321,7 +1321,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1321
1321
  matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
1322
1322
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1323
1323
  account: accountParam,
1324
- },
1324
+ }),
1325
1325
  }, async ({ docId, text, file, index, atEnd, at, occurrence, matchCase, tab, account }) => {
1326
1326
  const args = ['docs', 'header', 'create', docId];
1327
1327
  if (text !== undefined) args.push(`--text=${text}`);
@@ -1338,12 +1338,12 @@ export function registerExtraDocsTools(server: McpServer): void {
1338
1338
  server.registerTool('gog_docs_header_delete', {
1339
1339
  description: 'Delete a header from a Google Doc by its segment ID (from gog_docs_header_list).',
1340
1340
  annotations: { destructiveHint: true },
1341
- inputSchema: {
1341
+ inputSchema: z.object({
1342
1342
  docId: z.string().describe('Doc ID (from the URL)'),
1343
1343
  headerId: z.string().describe('Header segment ID (from gog_docs_header_list)'),
1344
1344
  tab: z.string().optional().describe('Tab title or ID containing the header'),
1345
1345
  account: accountParam,
1346
- },
1346
+ }),
1347
1347
  }, async ({ docId, headerId, tab, account }) => {
1348
1348
  const args = ['docs', 'header', 'delete', docId, headerId];
1349
1349
  if (tab) args.push(`--tab=${tab}`);
@@ -1354,11 +1354,11 @@ export function registerExtraDocsTools(server: McpServer): void {
1354
1354
  server.registerTool('gog_docs_footer_list', {
1355
1355
  description: 'List a Google Doc\'s footers and their segment IDs. Use a segment ID with the segment param on gog_docs_insert / gog_docs_update / gog_docs_delete / gog_docs_format / gog_docs_find_range to edit inside a footer.',
1356
1356
  annotations: { readOnlyHint: true },
1357
- inputSchema: {
1357
+ inputSchema: z.object({
1358
1358
  docId: z.string().describe('Doc ID (from the URL)'),
1359
1359
  tab: z.string().optional().describe('Limit results to a tab title or ID'),
1360
1360
  account: accountParam,
1361
- },
1361
+ }),
1362
1362
  }, async ({ docId, tab, account }) => {
1363
1363
  const args = ['docs', 'footer', 'list', docId];
1364
1364
  if (tab) args.push(`--tab=${tab}`);
@@ -1367,7 +1367,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1367
1367
 
1368
1368
  server.registerTool('gog_docs_footer_create', {
1369
1369
  description: 'Create a footer in a Google Doc and optionally populate its initial text. Returns the new footer segment ID.',
1370
- inputSchema: {
1370
+ inputSchema: z.object({
1371
1371
  docId: z.string().describe('Doc ID (from the URL)'),
1372
1372
  text: z.string().optional().describe('Initial footer text'),
1373
1373
  file: z.string().optional().describe('Read initial footer text from a file on the gog server. gog also accepts "-" for stdin, but this server never writes to gog\'s stdin, so "-" would hang until the call times out.'),
@@ -1378,7 +1378,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1378
1378
  matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
1379
1379
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1380
1380
  account: accountParam,
1381
- },
1381
+ }),
1382
1382
  }, async ({ docId, text, file, index, atEnd, at, occurrence, matchCase, tab, account }) => {
1383
1383
  const args = ['docs', 'footer', 'create', docId];
1384
1384
  if (text !== undefined) args.push(`--text=${text}`);
@@ -1395,12 +1395,12 @@ export function registerExtraDocsTools(server: McpServer): void {
1395
1395
  server.registerTool('gog_docs_footer_delete', {
1396
1396
  description: 'Delete a footer from a Google Doc by its segment ID (from gog_docs_footer_list).',
1397
1397
  annotations: { destructiveHint: true },
1398
- inputSchema: {
1398
+ inputSchema: z.object({
1399
1399
  docId: z.string().describe('Doc ID (from the URL)'),
1400
1400
  footerId: z.string().describe('Footer segment ID (from gog_docs_footer_list)'),
1401
1401
  tab: z.string().optional().describe('Tab title or ID containing the footer'),
1402
1402
  account: accountParam,
1403
- },
1403
+ }),
1404
1404
  }, async ({ docId, footerId, tab, account }) => {
1405
1405
  const args = ['docs', 'footer', 'delete', docId, footerId];
1406
1406
  if (tab) args.push(`--tab=${tab}`);
@@ -1413,7 +1413,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1413
1413
  server.registerTool('gog_docs_replace_image', {
1414
1414
  description: 'Replace an existing image in a Google Doc in place — keeping its position and bounds — with a new image from a local file or public HTTPS URL. Target the image by exact object ID (from gog_docs_images_list), by alt-text substring, or leave both off to replace the only image in the doc/tab.',
1415
1415
  annotations: { destructiveHint: true },
1416
- inputSchema: {
1416
+ inputSchema: z.object({
1417
1417
  docId: z.string().describe('Doc ID (from the URL)'),
1418
1418
  file: z.string().optional().describe('Local PNG, JPEG, or GIF image to upload and use (exactly one of file or url)'),
1419
1419
  url: z.string().optional().describe('Public HTTPS image URL to use directly — no Drive upload (exactly one of file or url)'),
@@ -1423,7 +1423,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1423
1423
  parent: z.string().optional().describe('Drive folder ID for an uploaded local image (file mode)'),
1424
1424
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1425
1425
  account: accountParam,
1426
- },
1426
+ }),
1427
1427
  }, async ({ docId, file, url, objectId, matchAlt, name, parent, tab, account }) => {
1428
1428
  const args = ['docs', 'replace-image', docId];
1429
1429
  if (file) args.push(`--file=${file}`);
@@ -1440,13 +1440,13 @@ export function registerExtraDocsTools(server: McpServer): void {
1440
1440
  server.registerTool('gog_docs_table_row_pin_header', {
1441
1441
  description: 'Pin (or unpin) leading rows of a native Google Docs table as repeating header rows. rows=N pins the first N rows; rows=0 unpins all header rows.',
1442
1442
  annotations: { destructiveHint: true },
1443
- inputSchema: {
1443
+ inputSchema: z.object({
1444
1444
  docId: z.string().describe('Doc ID (from the URL)'),
1445
1445
  rows: z.number().int().min(0).describe('Number of leading rows to pin as headers; 0 unpins all header rows'),
1446
1446
  table: tableSelectorParam,
1447
1447
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1448
1448
  account: accountParam,
1449
- },
1449
+ }),
1450
1450
  }, async ({ docId, rows, table, tab, account }) => {
1451
1451
  const args = ['docs', 'table-row', 'pin-header', docId, `--rows=${rows}`];
1452
1452
  if (table) args.push(`--table=${table}`);
@@ -1457,7 +1457,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1457
1457
  server.registerTool('gog_docs_table_row_style', {
1458
1458
  description: 'Set native table row height and page-overflow behaviour. Target a 1-based row (negative counts from the end) or omit row to style every row in the table.',
1459
1459
  annotations: { destructiveHint: true },
1460
- inputSchema: {
1460
+ inputSchema: z.object({
1461
1461
  docId: z.string().describe('Doc ID (from the URL)'),
1462
1462
  row: z.number().int().optional().describe('1-based row number; negative indexes count from the end; omit to style all rows'),
1463
1463
  minHeight: z.string().optional().describe('Minimum row height (points by default; supports pt, in, cm, mm)'),
@@ -1465,7 +1465,7 @@ export function registerExtraDocsTools(server: McpServer): void {
1465
1465
  table: tableSelectorParam,
1466
1466
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1467
1467
  account: accountParam,
1468
- },
1468
+ }),
1469
1469
  }, async ({ docId, row, minHeight, preventOverflow, table, tab, account }) => {
1470
1470
  const args = ['docs', 'table-row', 'style', docId];
1471
1471
  if (row !== undefined) args.push(`--row=${row}`);