gogcli-mcp-docs 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
10
- "version": "2.3.0"
10
+ "version": "2.4.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "gogcli (Docs)",
16
16
  "source": "./",
17
17
  "description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
18
- "version": "2.3.0",
18
+ "version": "2.4.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp-docs",
3
3
  "displayName": "gogcli (Docs)",
4
- "version": "2.3.0",
4
+ "version": "2.4.0",
5
5
  "description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/index.js CHANGED
@@ -31313,7 +31313,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
31313
31313
  );
31314
31314
 
31315
31315
  // ../gogcli-mcp/src/server.ts
31316
- var VERSION = true ? "2.3.0" : "0.0.0";
31316
+ var VERSION = true ? "2.4.0" : "0.0.0";
31317
31317
  function createServer(options) {
31318
31318
  return new McpServer({
31319
31319
  name: options?.name ?? "gogcli",
@@ -31543,15 +31543,19 @@ function registerExtraDocsTools(server2) {
31543
31543
  text: external_exports.string().optional().describe("Text content to write"),
31544
31544
  file: external_exports.string().optional().describe("Path to a file whose content to use"),
31545
31545
  index: external_exports.number().optional().describe("Character index to insert at"),
31546
+ replaceRange: external_exports.string().optional().describe('Replace a UTF-16 Docs API range START:END (e.g. "25:40") instead of inserting at index. The replacement text/file content overwrites the range.'),
31547
+ markdown: external_exports.boolean().optional().describe("Convert markdown in the text/file to Google Docs formatting (headings, bold, lists, etc.) instead of inserting it literally."),
31546
31548
  tabId: external_exports.string().optional().describe("Tab ID for multi-tab docs"),
31547
31549
  pageless: external_exports.boolean().optional().describe("Set document to pageless format"),
31548
31550
  account: accountParam
31549
31551
  }
31550
- }, async ({ docId, text, file: file2, index, tabId, pageless, account }) => {
31552
+ }, async ({ docId, text, file: file2, index, replaceRange, markdown, tabId, pageless, account }) => {
31551
31553
  const args = ["docs", "update", docId];
31552
31554
  if (text) args.push(`--text=${text}`);
31553
31555
  if (file2) args.push(`--file=${file2}`);
31554
31556
  if (index !== void 0) args.push(`--index=${index}`);
31557
+ if (replaceRange) args.push(`--replace-range=${replaceRange}`);
31558
+ if (markdown) args.push("--markdown");
31555
31559
  if (tabId) args.push(`--tab-id=${tabId}`);
31556
31560
  if (pageless) args.push("--pageless");
31557
31561
  return runOrDiagnose(args, { account });
@@ -31663,11 +31667,25 @@ function registerExtraDocsTools(server2) {
31663
31667
  inputSchema: {
31664
31668
  docId: external_exports.string().describe("Doc ID (from the URL)"),
31665
31669
  layout: external_exports.enum(["pageless", "pages"]).optional().describe("Page layout (default: pageless)"),
31670
+ pageSize: external_exports.enum(["A4", "A5", "Letter", "Legal", "Tabloid"]).optional().describe("Named page size preset (only applies in paged layout)"),
31671
+ pageWidth: external_exports.string().optional().describe('Page width (points by default; supports pt, in, cm, mm \u2014 e.g. "8.5in")'),
31672
+ pageHeight: external_exports.string().optional().describe("Page height (points by default; supports pt, in, cm, mm)"),
31673
+ marginTop: external_exports.string().optional().describe("Top page margin (points by default; supports pt, in, cm, mm)"),
31674
+ marginBottom: external_exports.string().optional().describe("Bottom page margin (points by default; supports pt, in, cm, mm)"),
31675
+ marginLeft: external_exports.string().optional().describe("Left page margin (points by default; supports pt, in, cm, mm)"),
31676
+ marginRight: external_exports.string().optional().describe("Right page margin (points by default; supports pt, in, cm, mm)"),
31666
31677
  account: accountParam
31667
31678
  }
31668
- }, async ({ docId, layout, account }) => {
31679
+ }, async ({ docId, layout, pageSize, pageWidth, pageHeight, marginTop, marginBottom, marginLeft, marginRight, account }) => {
31669
31680
  const args = ["docs", "page-layout", docId];
31670
31681
  if (layout) args.push(`--layout=${layout}`);
31682
+ if (pageSize) args.push(`--page-size=${pageSize}`);
31683
+ if (pageWidth) args.push(`--page-width=${pageWidth}`);
31684
+ if (pageHeight) args.push(`--page-height=${pageHeight}`);
31685
+ if (marginTop) args.push(`--margin-top=${marginTop}`);
31686
+ if (marginBottom) args.push(`--margin-bottom=${marginBottom}`);
31687
+ if (marginLeft) args.push(`--margin-left=${marginLeft}`);
31688
+ if (marginRight) args.push(`--margin-right=${marginRight}`);
31671
31689
  return runOrDiagnose(args, { account });
31672
31690
  });
31673
31691
  server2.registerTool("gog_docs_insert_table", {
@@ -31691,6 +31709,127 @@ function registerExtraDocsTools(server2) {
31691
31709
  if (tab) args.push(`--tab=${tab}`);
31692
31710
  return runOrDiagnose(args, { account });
31693
31711
  });
31712
+ server2.registerTool("gog_docs_cell_update", {
31713
+ description: "Replace (or append to) the content of a single table cell, addressed by table / row / column \u2014 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.",
31714
+ annotations: { destructiveHint: true },
31715
+ inputSchema: {
31716
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31717
+ row: external_exports.number().int().describe("1-based row number"),
31718
+ col: external_exports.number().int().describe("1-based column number"),
31719
+ content: external_exports.string().optional().describe("Replacement content (omit when using contentFile)"),
31720
+ contentFile: external_exports.string().optional().describe("Read replacement content from a file instead of content"),
31721
+ append: external_exports.boolean().optional().describe("Append inside the cell instead of replacing existing cell content"),
31722
+ format: external_exports.enum(["markdown", "plain"]).optional().describe("Content format (default: markdown)"),
31723
+ tableIndex: external_exports.number().int().optional().describe("1-based table index in document order; negative counts from the end (default: 1)"),
31724
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31725
+ account: accountParam
31726
+ }
31727
+ }, async ({ docId, row, col, content, contentFile, append, format, tableIndex, tab, account }) => {
31728
+ const args = ["docs", "cell-update", docId, `--row=${row}`, `--col=${col}`];
31729
+ if (content !== void 0) args.push(`--content=${content}`);
31730
+ if (contentFile) args.push(`--content-file=${contentFile}`);
31731
+ if (append) args.push("--append");
31732
+ if (format) args.push(`--format=${format}`);
31733
+ if (tableIndex !== void 0) args.push(`--table-index=${tableIndex}`);
31734
+ if (tab) args.push(`--tab=${tab}`);
31735
+ return runOrDiagnose(args, { account });
31736
+ });
31737
+ server2.registerTool("gog_docs_cell_style", {
31738
+ 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.",
31739
+ annotations: { destructiveHint: true },
31740
+ inputSchema: {
31741
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31742
+ row: external_exports.number().int().describe("0-based row number"),
31743
+ col: external_exports.number().int().describe("0-based column number"),
31744
+ rowSpan: external_exports.number().int().optional().describe("Number of rows to style (default: 1)"),
31745
+ colSpan: external_exports.number().int().optional().describe("Number of columns to style (default: 1)"),
31746
+ backgroundColor: external_exports.string().optional().describe("Cell background color as #RRGGBB or #RGB"),
31747
+ textColor: external_exports.string().optional().describe("Text color as #RRGGBB or #RGB"),
31748
+ bold: external_exports.boolean().optional().describe("Set cell text bold"),
31749
+ italic: external_exports.boolean().optional().describe("Set cell text italic"),
31750
+ underline: external_exports.boolean().optional().describe("Set cell text underline"),
31751
+ tableIndex: external_exports.number().int().optional().describe("0-based table index in document order (default: 0)"),
31752
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31753
+ account: accountParam
31754
+ }
31755
+ }, async ({ docId, row, col, rowSpan, colSpan, backgroundColor, textColor, bold, italic, underline, tableIndex, tab, account }) => {
31756
+ const args = ["docs", "cell-style", docId, `--row=${row}`, `--col=${col}`];
31757
+ if (rowSpan !== void 0) args.push(`--row-span=${rowSpan}`);
31758
+ if (colSpan !== void 0) args.push(`--col-span=${colSpan}`);
31759
+ if (backgroundColor) args.push(`--background-color=${backgroundColor}`);
31760
+ if (textColor) args.push(`--text-color=${textColor}`);
31761
+ if (bold) args.push("--bold");
31762
+ if (italic) args.push("--italic");
31763
+ if (underline) args.push("--underline");
31764
+ if (tableIndex !== void 0) args.push(`--table-index=${tableIndex}`);
31765
+ if (tab) args.push(`--tab=${tab}`);
31766
+ return runOrDiagnose(args, { account });
31767
+ });
31768
+ server2.registerTool("gog_docs_insert_image", {
31769
+ description: "Upload a local image (PNG, JPEG, or GIF) and insert it into a Google Doc. The image is uploaded to Drive, temporarily shared so Docs can fetch it, inserted, then the public permission is revoked. Replaces placeholder text (at) or appends at end-of-doc.",
31770
+ annotations: { destructiveHint: true },
31771
+ inputSchema: {
31772
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31773
+ file: external_exports.string().describe("Local PNG, JPEG, or GIF image to upload and insert"),
31774
+ at: external_exports.string().optional().describe('Placeholder text to replace, or "end" to append (default: end)'),
31775
+ width: external_exports.number().optional().describe("Image width in points (default: 468)"),
31776
+ height: external_exports.number().optional().describe("Image height in points (optional; width-only preserves aspect ratio)"),
31777
+ name: external_exports.string().optional().describe("Override the uploaded Drive filename"),
31778
+ parent: external_exports.string().optional().describe("Drive folder ID for the uploaded image"),
31779
+ onRestricted: external_exports.enum(["error", "link"]).optional().describe("If public sharing is blocked: error out, or fall back to a linked image (default: error)"),
31780
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31781
+ account: accountParam
31782
+ }
31783
+ }, async ({ docId, file: file2, at, width, height, name, parent, onRestricted, tab, account }) => {
31784
+ const args = ["docs", "insert-image", docId, `--file=${file2}`];
31785
+ if (at) args.push(`--at=${at}`);
31786
+ if (width !== void 0) args.push(`--width=${width}`);
31787
+ if (height !== void 0) args.push(`--height=${height}`);
31788
+ if (name) args.push(`--name=${name}`);
31789
+ if (parent) args.push(`--parent=${parent}`);
31790
+ if (onRestricted) args.push(`--on-restricted=${onRestricted}`);
31791
+ if (tab) args.push(`--tab=${tab}`);
31792
+ return runOrDiagnose(args, { account });
31793
+ });
31794
+ server2.registerTool("gog_docs_insert_person", {
31795
+ 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.",
31796
+ annotations: { destructiveHint: true },
31797
+ inputSchema: {
31798
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31799
+ email: external_exports.string().describe("Email address for the person chip"),
31800
+ index: external_exports.number().int().optional().describe("Character index to insert at. Omit or use atEnd for end-of-doc."),
31801
+ atEnd: external_exports.boolean().optional().describe("Insert at end-of-doc/tab (mutually exclusive with index)"),
31802
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31803
+ account: accountParam
31804
+ }
31805
+ }, async ({ docId, email: email3, index, atEnd, tab, account }) => {
31806
+ const args = ["docs", "insert-person", docId, `--email=${email3}`];
31807
+ if (index !== void 0) args.push(`--index=${index}`);
31808
+ if (atEnd) args.push("--at-end");
31809
+ if (tab) args.push(`--tab=${tab}`);
31810
+ return runOrDiagnose(args, { account });
31811
+ });
31812
+ server2.registerTool("gog_docs_insert_date_chip", {
31813
+ description: "Insert a native Google Docs date smart chip, at a character index or end-of-doc.",
31814
+ annotations: { destructiveHint: true },
31815
+ inputSchema: {
31816
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31817
+ date: external_exports.string().optional().describe("Date to insert as YYYY-MM-DD (default: today)"),
31818
+ format: external_exports.enum(["abbreviated", "full", "iso"]).optional().describe("Date display format (default: abbreviated)"),
31819
+ index: external_exports.number().int().optional().describe("Character index to insert at. Omit or use atEnd for end-of-doc."),
31820
+ atEnd: external_exports.boolean().optional().describe("Insert at end-of-doc/tab (mutually exclusive with index)"),
31821
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31822
+ account: accountParam
31823
+ }
31824
+ }, async ({ docId, date: date5, format, index, atEnd, tab, account }) => {
31825
+ const args = ["docs", "insert-date-chip", docId];
31826
+ if (date5) args.push(`--date=${date5}`);
31827
+ if (format) args.push(`--format=${format}`);
31828
+ if (index !== void 0) args.push(`--index=${index}`);
31829
+ if (atEnd) args.push("--at-end");
31830
+ if (tab) args.push(`--tab=${tab}`);
31831
+ return runOrDiagnose(args, { account });
31832
+ });
31694
31833
  server2.registerTool("gog_docs_add_tab", {
31695
31834
  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.",
31696
31835
  inputSchema: {
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-docs",
5
5
  "display_name": "gogcli (Docs)",
6
- "version": "2.3.0",
6
+ "version": "2.4.0",
7
7
  "description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -206,6 +206,26 @@
206
206
  {
207
207
  "name": "gog_docs_insert_table",
208
208
  "description": "Insert a native Google Docs table with given dimensions, optionally populated from a JSON 2D array."
209
+ },
210
+ {
211
+ "name": "gog_docs_cell_update",
212
+ "description": "Replace or append the content of a single table cell, addressed by table/row/column."
213
+ },
214
+ {
215
+ "name": "gog_docs_cell_style",
216
+ "description": "Apply background color and inline text styling to one or more table cells."
217
+ },
218
+ {
219
+ "name": "gog_docs_insert_image",
220
+ "description": "Upload a local image and insert it into a Google Doc."
221
+ },
222
+ {
223
+ "name": "gog_docs_insert_person",
224
+ "description": "Insert a native Google Docs person smart chip for an email address."
225
+ },
226
+ {
227
+ "name": "gog_docs_insert_date_chip",
228
+ "description": "Insert a native Google Docs date smart chip."
209
229
  }
210
230
  ],
211
231
  "compatibility": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-docs",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp-docs",
5
5
  "description": "Extended Google Docs MCP server via gogcli — all base tools plus full Docs 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-docs"
9
9
  },
10
- "version": "2.3.0",
10
+ "version": "2.4.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-docs",
15
- "version": "2.3.0",
15
+ "version": "2.4.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -253,15 +253,19 @@ export function registerExtraDocsTools(server: McpServer): void {
253
253
  text: z.string().optional().describe('Text content to write'),
254
254
  file: z.string().optional().describe('Path to a file whose content to use'),
255
255
  index: z.number().optional().describe('Character index to insert at'),
256
+ replaceRange: z.string().optional().describe('Replace a UTF-16 Docs API range START:END (e.g. "25:40") instead of inserting at index. The replacement text/file content overwrites the range.'),
257
+ markdown: z.boolean().optional().describe('Convert markdown in the text/file to Google Docs formatting (headings, bold, lists, etc.) instead of inserting it literally.'),
256
258
  tabId: z.string().optional().describe('Tab ID for multi-tab docs'),
257
259
  pageless: z.boolean().optional().describe('Set document to pageless format'),
258
260
  account: accountParam,
259
261
  },
260
- }, async ({ docId, text, file, index, tabId, pageless, account }) => {
262
+ }, async ({ docId, text, file, index, replaceRange, markdown, tabId, pageless, account }) => {
261
263
  const args = ['docs', 'update', docId];
262
264
  if (text) args.push(`--text=${text}`);
263
265
  if (file) args.push(`--file=${file}`);
264
266
  if (index !== undefined) args.push(`--index=${index}`);
267
+ if (replaceRange) args.push(`--replace-range=${replaceRange}`);
268
+ if (markdown) args.push('--markdown');
265
269
  if (tabId) args.push(`--tab-id=${tabId}`);
266
270
  if (pageless) args.push('--pageless');
267
271
  return runOrDiagnose(args, { account });
@@ -385,11 +389,25 @@ export function registerExtraDocsTools(server: McpServer): void {
385
389
  inputSchema: {
386
390
  docId: z.string().describe('Doc ID (from the URL)'),
387
391
  layout: z.enum(['pageless', 'pages']).optional().describe('Page layout (default: pageless)'),
392
+ pageSize: z.enum(['A4', 'A5', 'Letter', 'Legal', 'Tabloid']).optional().describe('Named page size preset (only applies in paged layout)'),
393
+ pageWidth: z.string().optional().describe('Page width (points by default; supports pt, in, cm, mm — e.g. "8.5in")'),
394
+ pageHeight: z.string().optional().describe('Page height (points by default; supports pt, in, cm, mm)'),
395
+ marginTop: z.string().optional().describe('Top page margin (points by default; supports pt, in, cm, mm)'),
396
+ marginBottom: z.string().optional().describe('Bottom page margin (points by default; supports pt, in, cm, mm)'),
397
+ marginLeft: z.string().optional().describe('Left page margin (points by default; supports pt, in, cm, mm)'),
398
+ marginRight: z.string().optional().describe('Right page margin (points by default; supports pt, in, cm, mm)'),
388
399
  account: accountParam,
389
400
  },
390
- }, async ({ docId, layout, account }) => {
401
+ }, async ({ docId, layout, pageSize, pageWidth, pageHeight, marginTop, marginBottom, marginLeft, marginRight, account }) => {
391
402
  const args = ['docs', 'page-layout', docId];
392
403
  if (layout) args.push(`--layout=${layout}`);
404
+ if (pageSize) args.push(`--page-size=${pageSize}`);
405
+ if (pageWidth) args.push(`--page-width=${pageWidth}`);
406
+ if (pageHeight) args.push(`--page-height=${pageHeight}`);
407
+ if (marginTop) args.push(`--margin-top=${marginTop}`);
408
+ if (marginBottom) args.push(`--margin-bottom=${marginBottom}`);
409
+ if (marginLeft) args.push(`--margin-left=${marginLeft}`);
410
+ if (marginRight) args.push(`--margin-right=${marginRight}`);
393
411
  return runOrDiagnose(args, { account });
394
412
  });
395
413
 
@@ -415,6 +433,132 @@ export function registerExtraDocsTools(server: McpServer): void {
415
433
  return runOrDiagnose(args, { account });
416
434
  });
417
435
 
436
+ server.registerTool('gog_docs_cell_update', {
437
+ 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.',
438
+ annotations: { destructiveHint: true },
439
+ inputSchema: {
440
+ docId: z.string().describe('Doc ID (from the URL)'),
441
+ row: z.number().int().describe('1-based row number'),
442
+ col: z.number().int().describe('1-based column number'),
443
+ content: z.string().optional().describe('Replacement content (omit when using contentFile)'),
444
+ contentFile: z.string().optional().describe('Read replacement content from a file instead of content'),
445
+ append: z.boolean().optional().describe('Append inside the cell instead of replacing existing cell content'),
446
+ format: z.enum(['markdown', 'plain']).optional().describe('Content format (default: markdown)'),
447
+ tableIndex: z.number().int().optional().describe('1-based table index in document order; negative counts from the end (default: 1)'),
448
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
449
+ account: accountParam,
450
+ },
451
+ }, async ({ docId, row, col, content, contentFile, append, format, tableIndex, tab, account }) => {
452
+ const args = ['docs', 'cell-update', docId, `--row=${row}`, `--col=${col}`];
453
+ if (content !== undefined) args.push(`--content=${content}`);
454
+ if (contentFile) args.push(`--content-file=${contentFile}`);
455
+ if (append) args.push('--append');
456
+ if (format) args.push(`--format=${format}`);
457
+ if (tableIndex !== undefined) args.push(`--table-index=${tableIndex}`);
458
+ if (tab) args.push(`--tab=${tab}`);
459
+ return runOrDiagnose(args, { account });
460
+ });
461
+
462
+ server.registerTool('gog_docs_cell_style', {
463
+ 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.',
464
+ annotations: { destructiveHint: true },
465
+ inputSchema: {
466
+ docId: z.string().describe('Doc ID (from the URL)'),
467
+ row: z.number().int().describe('0-based row number'),
468
+ col: z.number().int().describe('0-based column number'),
469
+ rowSpan: z.number().int().optional().describe('Number of rows to style (default: 1)'),
470
+ colSpan: z.number().int().optional().describe('Number of columns to style (default: 1)'),
471
+ backgroundColor: z.string().optional().describe('Cell background color as #RRGGBB or #RGB'),
472
+ textColor: z.string().optional().describe('Text color as #RRGGBB or #RGB'),
473
+ bold: z.boolean().optional().describe('Set cell text bold'),
474
+ italic: z.boolean().optional().describe('Set cell text italic'),
475
+ underline: z.boolean().optional().describe('Set cell text underline'),
476
+ tableIndex: z.number().int().optional().describe('0-based table index in document order (default: 0)'),
477
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
478
+ account: accountParam,
479
+ },
480
+ }, async ({ docId, row, col, rowSpan, colSpan, backgroundColor, textColor, bold, italic, underline, tableIndex, tab, account }) => {
481
+ const args = ['docs', 'cell-style', docId, `--row=${row}`, `--col=${col}`];
482
+ if (rowSpan !== undefined) args.push(`--row-span=${rowSpan}`);
483
+ if (colSpan !== undefined) args.push(`--col-span=${colSpan}`);
484
+ if (backgroundColor) args.push(`--background-color=${backgroundColor}`);
485
+ if (textColor) args.push(`--text-color=${textColor}`);
486
+ if (bold) args.push('--bold');
487
+ if (italic) args.push('--italic');
488
+ if (underline) args.push('--underline');
489
+ if (tableIndex !== undefined) args.push(`--table-index=${tableIndex}`);
490
+ if (tab) args.push(`--tab=${tab}`);
491
+ return runOrDiagnose(args, { account });
492
+ });
493
+
494
+ server.registerTool('gog_docs_insert_image', {
495
+ description: 'Upload a local image (PNG, JPEG, or GIF) and insert it into a Google Doc. The image is uploaded to Drive, temporarily shared so Docs can fetch it, inserted, then the public permission is revoked. Replaces placeholder text (at) or appends at end-of-doc.',
496
+ annotations: { destructiveHint: true },
497
+ inputSchema: {
498
+ docId: z.string().describe('Doc ID (from the URL)'),
499
+ file: z.string().describe('Local PNG, JPEG, or GIF image to upload and insert'),
500
+ at: z.string().optional().describe('Placeholder text to replace, or "end" to append (default: end)'),
501
+ width: z.number().optional().describe('Image width in points (default: 468)'),
502
+ height: z.number().optional().describe('Image height in points (optional; width-only preserves aspect ratio)'),
503
+ name: z.string().optional().describe('Override the uploaded Drive filename'),
504
+ parent: z.string().optional().describe('Drive folder ID for the uploaded image'),
505
+ onRestricted: z.enum(['error', 'link']).optional().describe('If public sharing is blocked: error out, or fall back to a linked image (default: error)'),
506
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
507
+ account: accountParam,
508
+ },
509
+ }, async ({ docId, file, at, width, height, name, parent, onRestricted, tab, account }) => {
510
+ const args = ['docs', 'insert-image', docId, `--file=${file}`];
511
+ if (at) args.push(`--at=${at}`);
512
+ if (width !== undefined) args.push(`--width=${width}`);
513
+ if (height !== undefined) args.push(`--height=${height}`);
514
+ if (name) args.push(`--name=${name}`);
515
+ if (parent) args.push(`--parent=${parent}`);
516
+ if (onRestricted) args.push(`--on-restricted=${onRestricted}`);
517
+ if (tab) args.push(`--tab=${tab}`);
518
+ return runOrDiagnose(args, { account });
519
+ });
520
+
521
+ server.registerTool('gog_docs_insert_person', {
522
+ 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.',
523
+ annotations: { destructiveHint: true },
524
+ inputSchema: {
525
+ docId: z.string().describe('Doc ID (from the URL)'),
526
+ email: z.string().describe('Email address for the person chip'),
527
+ index: z.number().int().optional().describe('Character index to insert at. Omit or use atEnd for end-of-doc.'),
528
+ atEnd: z.boolean().optional().describe('Insert at end-of-doc/tab (mutually exclusive with index)'),
529
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
530
+ account: accountParam,
531
+ },
532
+ }, async ({ docId, email, index, atEnd, tab, account }) => {
533
+ const args = ['docs', 'insert-person', docId, `--email=${email}`];
534
+ if (index !== undefined) args.push(`--index=${index}`);
535
+ if (atEnd) args.push('--at-end');
536
+ if (tab) args.push(`--tab=${tab}`);
537
+ return runOrDiagnose(args, { account });
538
+ });
539
+
540
+ server.registerTool('gog_docs_insert_date_chip', {
541
+ description: 'Insert a native Google Docs date smart chip, at a character index or end-of-doc.',
542
+ annotations: { destructiveHint: true },
543
+ inputSchema: {
544
+ docId: z.string().describe('Doc ID (from the URL)'),
545
+ date: z.string().optional().describe('Date to insert as YYYY-MM-DD (default: today)'),
546
+ format: z.enum(['abbreviated', 'full', 'iso']).optional().describe('Date display format (default: abbreviated)'),
547
+ index: z.number().int().optional().describe('Character index to insert at. Omit or use atEnd for end-of-doc.'),
548
+ atEnd: z.boolean().optional().describe('Insert at end-of-doc/tab (mutually exclusive with index)'),
549
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
550
+ account: accountParam,
551
+ },
552
+ }, async ({ docId, date, format, index, atEnd, tab, account }) => {
553
+ const args = ['docs', 'insert-date-chip', docId];
554
+ if (date) args.push(`--date=${date}`);
555
+ if (format) args.push(`--format=${format}`);
556
+ if (index !== undefined) args.push(`--index=${index}`);
557
+ if (atEnd) args.push('--at-end');
558
+ if (tab) args.push(`--tab=${tab}`);
559
+ return runOrDiagnose(args, { account });
560
+ });
561
+
418
562
  server.registerTool('gog_docs_add_tab', {
419
563
  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.',
420
564
  inputSchema: {
@@ -378,6 +378,36 @@ describe('gog_docs_update', () => {
378
378
  await handlers.get('gog_docs_update')!({ docId: 'abc' });
379
379
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'update', 'abc'], { account: undefined });
380
380
  });
381
+
382
+ it('includes --replace-range when provided', async () => {
383
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
384
+ const handlers = setupHandlers();
385
+ await handlers.get('gog_docs_update')!({ docId: 'abc', text: 'new', replaceRange: '25:40' });
386
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
387
+ ['docs', 'update', 'abc', '--text=new', '--replace-range=25:40'],
388
+ { account: undefined },
389
+ );
390
+ });
391
+
392
+ it('includes --markdown when true', async () => {
393
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
394
+ const handlers = setupHandlers();
395
+ await handlers.get('gog_docs_update')!({ docId: 'abc', text: '# Heading', markdown: true });
396
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
397
+ ['docs', 'update', 'abc', '--text=# Heading', '--markdown'],
398
+ { account: undefined },
399
+ );
400
+ });
401
+
402
+ it('omits --markdown when false', async () => {
403
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
404
+ const handlers = setupHandlers();
405
+ await handlers.get('gog_docs_update')!({ docId: 'abc', text: 'hi', markdown: false });
406
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
407
+ ['docs', 'update', 'abc', '--text=hi'],
408
+ { account: undefined },
409
+ );
410
+ });
381
411
  });
382
412
 
383
413
  // --- Dedicated comment tools ---
@@ -741,6 +771,160 @@ describe('gog_docs_page_layout', () => {
741
771
  await handlers.get('gog_docs_page_layout')!({ docId: 'd1' });
742
772
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'page-layout', 'd1'], { account: undefined });
743
773
  });
774
+
775
+ it('passes --page-size, page dimensions and all margins', async () => {
776
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
777
+ const handlers = setupHandlers();
778
+ await handlers.get('gog_docs_page_layout')!({
779
+ docId: 'd1', layout: 'pages', pageSize: 'A4', pageWidth: '8.5in', pageHeight: '11in',
780
+ marginTop: '1in', marginBottom: '1in', marginLeft: '0.75in', marginRight: '0.75in',
781
+ });
782
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
783
+ ['docs', 'page-layout', 'd1', '--layout=pages', '--page-size=A4', '--page-width=8.5in',
784
+ '--page-height=11in', '--margin-top=1in', '--margin-bottom=1in', '--margin-left=0.75in', '--margin-right=0.75in'],
785
+ { account: undefined },
786
+ );
787
+ });
788
+ });
789
+
790
+ describe('gog_docs_cell_update', () => {
791
+ it('passes required --row and --col', async () => {
792
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
793
+ const handlers = setupHandlers();
794
+ await handlers.get('gog_docs_cell_update')!({ docId: 'd1', row: 2, col: 3, content: 'hi' });
795
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
796
+ ['docs', 'cell-update', 'd1', '--row=2', '--col=3', '--content=hi'],
797
+ { account: undefined },
798
+ );
799
+ });
800
+
801
+ it('supports empty-string content, --content-file, --append, --format, --table-index and --tab', async () => {
802
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
803
+ const handlers = setupHandlers();
804
+ await handlers.get('gog_docs_cell_update')!({
805
+ docId: 'd1', row: 1, col: 1, content: '', contentFile: '/tmp/c.md', append: true,
806
+ format: 'plain', tableIndex: -1, tab: 'Tab2',
807
+ });
808
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
809
+ ['docs', 'cell-update', 'd1', '--row=1', '--col=1', '--content=', '--content-file=/tmp/c.md',
810
+ '--append', '--format=plain', '--table-index=-1', '--tab=Tab2'],
811
+ { account: undefined },
812
+ );
813
+ });
814
+
815
+ it('omits content when not provided and forwards account', async () => {
816
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
817
+ const handlers = setupHandlers();
818
+ await handlers.get('gog_docs_cell_update')!({ docId: 'd1', row: 1, col: 2, account: 'a@b.com' });
819
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
820
+ ['docs', 'cell-update', 'd1', '--row=1', '--col=2'],
821
+ { account: 'a@b.com' },
822
+ );
823
+ });
824
+ });
825
+
826
+ describe('gog_docs_cell_style', () => {
827
+ it('passes required --row and --col', async () => {
828
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
829
+ const handlers = setupHandlers();
830
+ await handlers.get('gog_docs_cell_style')!({ docId: 'd1', row: 0, col: 0 });
831
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
832
+ ['docs', 'cell-style', 'd1', '--row=0', '--col=0'],
833
+ { account: undefined },
834
+ );
835
+ });
836
+
837
+ it('passes spans, colors and text styling', async () => {
838
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
839
+ const handlers = setupHandlers();
840
+ await handlers.get('gog_docs_cell_style')!({
841
+ docId: 'd1', row: 1, col: 2, rowSpan: 2, colSpan: 3, backgroundColor: '#eee',
842
+ textColor: '#111', bold: true, italic: true, underline: true, tableIndex: 1, tab: 'T',
843
+ });
844
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
845
+ ['docs', 'cell-style', 'd1', '--row=1', '--col=2', '--row-span=2', '--col-span=3',
846
+ '--background-color=#eee', '--text-color=#111', '--bold', '--italic', '--underline',
847
+ '--table-index=1', '--tab=T'],
848
+ { account: undefined },
849
+ );
850
+ });
851
+
852
+ it('omits text styling flags when false', async () => {
853
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
854
+ const handlers = setupHandlers();
855
+ await handlers.get('gog_docs_cell_style')!({ docId: 'd1', row: 0, col: 0, bold: false, italic: false, underline: false });
856
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
857
+ ['docs', 'cell-style', 'd1', '--row=0', '--col=0'],
858
+ { account: undefined },
859
+ );
860
+ });
861
+ });
862
+
863
+ describe('gog_docs_insert_image', () => {
864
+ it('passes required --file and defaults', async () => {
865
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
866
+ const handlers = setupHandlers();
867
+ await handlers.get('gog_docs_insert_image')!({ docId: 'd1', file: '/tmp/pic.png' });
868
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
869
+ ['docs', 'insert-image', 'd1', '--file=/tmp/pic.png'],
870
+ { account: undefined },
871
+ );
872
+ });
873
+
874
+ it('passes placement, sizing, naming and restriction handling', async () => {
875
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
876
+ const handlers = setupHandlers();
877
+ await handlers.get('gog_docs_insert_image')!({
878
+ docId: 'd1', file: '/tmp/pic.png', at: '{{logo}}', width: 200, height: 100,
879
+ name: 'logo.png', parent: 'folder1', onRestricted: 'link', tab: 'T',
880
+ });
881
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
882
+ ['docs', 'insert-image', 'd1', '--file=/tmp/pic.png', '--at={{logo}}', '--width=200',
883
+ '--height=100', '--name=logo.png', '--parent=folder1', '--on-restricted=link', '--tab=T'],
884
+ { account: undefined },
885
+ );
886
+ });
887
+ });
888
+
889
+ describe('gog_docs_insert_person', () => {
890
+ it('passes required --email', async () => {
891
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
892
+ const handlers = setupHandlers();
893
+ await handlers.get('gog_docs_insert_person')!({ docId: 'd1', email: 'a@b.com' });
894
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
895
+ ['docs', 'insert-person', 'd1', '--email=a@b.com'],
896
+ { account: undefined },
897
+ );
898
+ });
899
+
900
+ it('passes --index, --at-end and --tab', async () => {
901
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
902
+ const handlers = setupHandlers();
903
+ await handlers.get('gog_docs_insert_person')!({ docId: 'd1', email: 'a@b.com', index: 0, atEnd: true, tab: 'T' });
904
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
905
+ ['docs', 'insert-person', 'd1', '--email=a@b.com', '--index=0', '--at-end', '--tab=T'],
906
+ { account: undefined },
907
+ );
908
+ });
909
+ });
910
+
911
+ describe('gog_docs_insert_date_chip', () => {
912
+ it('omits all optional flags by default', async () => {
913
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
914
+ const handlers = setupHandlers();
915
+ await handlers.get('gog_docs_insert_date_chip')!({ docId: 'd1' });
916
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'insert-date-chip', 'd1'], { account: undefined });
917
+ });
918
+
919
+ it('passes --date, --format, --index and --at-end', async () => {
920
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
921
+ const handlers = setupHandlers();
922
+ await handlers.get('gog_docs_insert_date_chip')!({ docId: 'd1', date: '2026-06-01', format: 'iso', index: 5, atEnd: true });
923
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
924
+ ['docs', 'insert-date-chip', 'd1', '--date=2026-06-01', '--format=iso', '--index=5', '--at-end'],
925
+ { account: undefined },
926
+ );
927
+ });
744
928
  });
745
929
 
746
930
  describe('gog_docs_insert_table', () => {