gogcli-mcp-docs 2.0.9 → 2.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +105 -4
- package/manifest.json +17 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/tools/docs-extra.ts +122 -3
- package/tests/tools/docs-extra.test.ts +126 -0
|
@@ -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.0.
|
|
10
|
+
"version": "2.0.10"
|
|
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.0.
|
|
18
|
+
"version": "2.0.10",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/index.js
CHANGED
|
@@ -31288,8 +31288,11 @@ function registerDocsTools(server2) {
|
|
|
31288
31288
|
registerRunTool(server2, { service: "docs", examples: '"copy", "clear", "insert", "sed", "export"' });
|
|
31289
31289
|
}
|
|
31290
31290
|
|
|
31291
|
+
// ../gogcli-mcp/src/tools/sheets.ts
|
|
31292
|
+
var cellValueParam = external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]);
|
|
31293
|
+
|
|
31291
31294
|
// ../gogcli-mcp/src/server.ts
|
|
31292
|
-
var VERSION = true ? "2.0.
|
|
31295
|
+
var VERSION = true ? "2.0.10" : "0.0.0";
|
|
31293
31296
|
function createServer(options) {
|
|
31294
31297
|
return new McpServer({
|
|
31295
31298
|
name: options?.name ?? "gogcli",
|
|
@@ -31313,7 +31316,7 @@ function registerExtraDocsTools(server2) {
|
|
|
31313
31316
|
return runOrDiagnose(args, { account });
|
|
31314
31317
|
});
|
|
31315
31318
|
server2.registerTool("gog_docs_delete", {
|
|
31316
|
-
description: "Delete content within a Google Doc by character index range.",
|
|
31319
|
+
description: "Delete content within a Google Doc by character index range. To remove the entire document (move to Drive trash), use gog_docs_trash.",
|
|
31317
31320
|
annotations: { destructiveHint: true },
|
|
31318
31321
|
inputSchema: {
|
|
31319
31322
|
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
@@ -31327,6 +31330,16 @@ function registerExtraDocsTools(server2) {
|
|
|
31327
31330
|
if (tabId) args.push(`--tab-id=${tabId}`);
|
|
31328
31331
|
return runOrDiagnose(args, { account });
|
|
31329
31332
|
});
|
|
31333
|
+
server2.registerTool("gog_docs_trash", {
|
|
31334
|
+
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.",
|
|
31335
|
+
annotations: { destructiveHint: true },
|
|
31336
|
+
inputSchema: {
|
|
31337
|
+
docId: external_exports.string().describe("Doc ID to move to trash"),
|
|
31338
|
+
account: accountParam
|
|
31339
|
+
}
|
|
31340
|
+
}, async ({ docId, account }) => {
|
|
31341
|
+
return runOrDiagnose(["drive", "delete", docId], { account });
|
|
31342
|
+
});
|
|
31330
31343
|
server2.registerTool("gog_docs_edit", {
|
|
31331
31344
|
description: "Edit a Google Doc by finding and replacing text (stream-edit style).",
|
|
31332
31345
|
annotations: { destructiveHint: true },
|
|
@@ -31342,6 +31355,75 @@ function registerExtraDocsTools(server2) {
|
|
|
31342
31355
|
if (matchCase) args.push("--match-case");
|
|
31343
31356
|
return runOrDiagnose(args, { account });
|
|
31344
31357
|
});
|
|
31358
|
+
server2.registerTool("gog_docs_read", {
|
|
31359
|
+
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" \u2014 it writes to a file. Use gog_docs_structure to see paragraph-by-paragraph layout with indices.',
|
|
31360
|
+
annotations: { readOnlyHint: true },
|
|
31361
|
+
inputSchema: {
|
|
31362
|
+
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31363
|
+
format: external_exports.enum(["text", "json"]).optional().describe("Output format (default: text)"),
|
|
31364
|
+
tab: external_exports.string().optional().describe("Target tab title or ID (text mode only)"),
|
|
31365
|
+
allTabs: external_exports.boolean().optional().describe("Show all tabs with headers (text mode only)"),
|
|
31366
|
+
maxBytes: external_exports.number().optional().describe("Max bytes to read in text mode (0 = unlimited; default 2000000)"),
|
|
31367
|
+
account: accountParam
|
|
31368
|
+
}
|
|
31369
|
+
}, async ({ docId, format, tab, allTabs, maxBytes, account }) => {
|
|
31370
|
+
if (format === "json") {
|
|
31371
|
+
return runOrDiagnose(["docs", "raw", docId, "--pretty"], { account });
|
|
31372
|
+
}
|
|
31373
|
+
const args = ["docs", "cat", docId];
|
|
31374
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
31375
|
+
if (allTabs) args.push("--all-tabs");
|
|
31376
|
+
if (maxBytes !== void 0) args.push(`--max-bytes=${maxBytes}`);
|
|
31377
|
+
return runOrDiagnose(args, { account });
|
|
31378
|
+
});
|
|
31379
|
+
server2.registerTool("gog_docs_format", {
|
|
31380
|
+
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.",
|
|
31381
|
+
annotations: { destructiveHint: true },
|
|
31382
|
+
inputSchema: {
|
|
31383
|
+
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31384
|
+
match: external_exports.string().optional().describe("Format only the first text match"),
|
|
31385
|
+
matchAll: external_exports.boolean().optional().describe("Format all matches instead of only the first"),
|
|
31386
|
+
matchCase: external_exports.boolean().optional().describe("Case-sensitive matching"),
|
|
31387
|
+
tab: external_exports.string().optional().describe("Target tab title or ID"),
|
|
31388
|
+
fontFamily: external_exports.string().optional().describe("Font family (e.g. Arial, Georgia)"),
|
|
31389
|
+
fontSize: external_exports.number().optional().describe("Font size in points"),
|
|
31390
|
+
textColor: external_exports.string().optional().describe("Text color as #RRGGBB or #RGB"),
|
|
31391
|
+
bgColor: external_exports.string().optional().describe("Text background color as #RRGGBB or #RGB"),
|
|
31392
|
+
bold: external_exports.boolean().optional().describe("Set bold"),
|
|
31393
|
+
noBold: external_exports.boolean().optional().describe("Clear bold"),
|
|
31394
|
+
italic: external_exports.boolean().optional().describe("Set italic"),
|
|
31395
|
+
noItalic: external_exports.boolean().optional().describe("Clear italic"),
|
|
31396
|
+
underline: external_exports.boolean().optional().describe("Set underline"),
|
|
31397
|
+
noUnderline: external_exports.boolean().optional().describe("Clear underline"),
|
|
31398
|
+
strikethrough: external_exports.boolean().optional().describe("Set strikethrough"),
|
|
31399
|
+
noStrikethrough: external_exports.boolean().optional().describe("Clear strikethrough"),
|
|
31400
|
+
alignment: external_exports.enum(["left", "center", "right", "justify", "start", "end", "justified"]).optional().describe("Paragraph alignment"),
|
|
31401
|
+
lineSpacing: external_exports.number().optional().describe("Line spacing percentage (e.g. 100 for single, 150 for 1.5x, 200 for double)"),
|
|
31402
|
+
account: accountParam
|
|
31403
|
+
}
|
|
31404
|
+
}, async (args) => {
|
|
31405
|
+
const a = args;
|
|
31406
|
+
const argv = ["docs", "format", a.docId];
|
|
31407
|
+
if (a.match) argv.push(`--match=${a.match}`);
|
|
31408
|
+
if (a.matchAll) argv.push("--match-all");
|
|
31409
|
+
if (a.matchCase) argv.push("--match-case");
|
|
31410
|
+
if (a.tab) argv.push(`--tab=${a.tab}`);
|
|
31411
|
+
if (a.fontFamily) argv.push(`--font-family=${a.fontFamily}`);
|
|
31412
|
+
if (a.fontSize !== void 0) argv.push(`--font-size=${a.fontSize}`);
|
|
31413
|
+
if (a.textColor) argv.push(`--text-color=${a.textColor}`);
|
|
31414
|
+
if (a.bgColor) argv.push(`--bg-color=${a.bgColor}`);
|
|
31415
|
+
if (a.bold) argv.push("--bold");
|
|
31416
|
+
if (a.noBold) argv.push("--no-bold");
|
|
31417
|
+
if (a.italic) argv.push("--italic");
|
|
31418
|
+
if (a.noItalic) argv.push("--no-italic");
|
|
31419
|
+
if (a.underline) argv.push("--underline");
|
|
31420
|
+
if (a.noUnderline) argv.push("--no-underline");
|
|
31421
|
+
if (a.strikethrough) argv.push("--strikethrough");
|
|
31422
|
+
if (a.noStrikethrough) argv.push("--no-strikethrough");
|
|
31423
|
+
if (a.alignment) argv.push(`--alignment=${a.alignment}`);
|
|
31424
|
+
if (a.lineSpacing !== void 0) argv.push(`--line-spacing=${a.lineSpacing}`);
|
|
31425
|
+
return runOrDiagnose(argv, { account: a.account });
|
|
31426
|
+
});
|
|
31345
31427
|
server2.registerTool("gog_docs_export", {
|
|
31346
31428
|
description: "Export a Google Doc as PDF, plain text, HTML, DOCX, or other format.",
|
|
31347
31429
|
annotations: { readOnlyHint: true },
|
|
@@ -31358,12 +31440,12 @@ function registerExtraDocsTools(server2) {
|
|
|
31358
31440
|
return runOrDiagnose(args, { account });
|
|
31359
31441
|
});
|
|
31360
31442
|
server2.registerTool("gog_docs_insert", {
|
|
31361
|
-
description: "Insert text at a specific
|
|
31443
|
+
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 \u2014 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.",
|
|
31362
31444
|
annotations: { destructiveHint: true },
|
|
31363
31445
|
inputSchema: {
|
|
31364
31446
|
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31365
31447
|
content: external_exports.string().optional().describe("Text content to insert"),
|
|
31366
|
-
index: external_exports.number().optional().describe("Character index to insert at (default:
|
|
31448
|
+
index: external_exports.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."),
|
|
31367
31449
|
file: external_exports.string().optional().describe("Path to a file whose content to insert"),
|
|
31368
31450
|
tabId: external_exports.string().optional().describe("Tab ID to insert into (for multi-tab docs)"),
|
|
31369
31451
|
account: accountParam
|
|
@@ -31376,6 +31458,25 @@ function registerExtraDocsTools(server2) {
|
|
|
31376
31458
|
if (tabId) args.push(`--tab-id=${tabId}`);
|
|
31377
31459
|
return runOrDiagnose(args, { account });
|
|
31378
31460
|
});
|
|
31461
|
+
server2.registerTool("gog_docs_append", {
|
|
31462
|
+
description: "Append text to the end of a Google Doc. This is the right tool for iterative document construction \u2014 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.",
|
|
31463
|
+
annotations: { destructiveHint: true },
|
|
31464
|
+
inputSchema: {
|
|
31465
|
+
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31466
|
+
text: external_exports.string().optional().describe("Text content to append"),
|
|
31467
|
+
file: external_exports.string().optional().describe('Path to a text file to append (use "-" for stdin)'),
|
|
31468
|
+
markdown: external_exports.boolean().optional().describe("Convert markdown to Google Docs formatting (headings, bold, lists, etc.)"),
|
|
31469
|
+
tab: external_exports.string().optional().describe("Target tab title or ID (for multi-tab docs)"),
|
|
31470
|
+
account: accountParam
|
|
31471
|
+
}
|
|
31472
|
+
}, async ({ docId, text, file: file2, markdown, tab, account }) => {
|
|
31473
|
+
const args = ["docs", "write", docId, "--append"];
|
|
31474
|
+
if (text) args.push(`--text=${text}`);
|
|
31475
|
+
if (file2) args.push(`--file=${file2}`);
|
|
31476
|
+
if (markdown) args.push("--markdown");
|
|
31477
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
31478
|
+
return runOrDiagnose(args, { account });
|
|
31479
|
+
});
|
|
31379
31480
|
server2.registerTool("gog_docs_list_tabs", {
|
|
31380
31481
|
description: "List all tabs in a Google Doc.",
|
|
31381
31482
|
annotations: { readOnlyHint: true },
|
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.0.
|
|
6
|
+
"version": "2.0.10",
|
|
7
7
|
"description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -158,6 +158,22 @@
|
|
|
158
158
|
{
|
|
159
159
|
"name": "gog_docs_comments_delete",
|
|
160
160
|
"description": "Delete a comment from a Google Doc. This action is permanent."
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "gog_docs_append",
|
|
164
|
+
"description": "Append text to the end of a Google Doc (the right tool for iterative construction; sequential calls produce in-order content)"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "gog_docs_read",
|
|
168
|
+
"description": "Read a Google Doc as plain text or as raw Docs API JSON"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "gog_docs_format",
|
|
172
|
+
"description": "Apply text/paragraph formatting (bold, italic, font size, color, alignment, line spacing) — supports match, matchAll, matchCase"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"name": "gog_docs_trash",
|
|
176
|
+
"description": "Move an entire Google Doc to Drive trash (convenience wrapper around gog drive delete)"
|
|
161
177
|
}
|
|
162
178
|
],
|
|
163
179
|
"compatibility": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-docs",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
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.0.
|
|
10
|
+
"version": "2.0.10",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp-docs",
|
|
15
|
-
"version": "2.0.
|
|
15
|
+
"version": "2.0.10",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/src/tools/docs-extra.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
server.registerTool('gog_docs_delete', {
|
|
21
|
-
description: 'Delete content within a Google Doc by character index range.',
|
|
21
|
+
description: 'Delete content within a Google Doc by character index range. To remove the entire document (move to Drive trash), use gog_docs_trash.',
|
|
22
22
|
annotations: { destructiveHint: true },
|
|
23
23
|
inputSchema: {
|
|
24
24
|
docId: z.string().describe('Doc ID (from the URL)'),
|
|
@@ -33,6 +33,17 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
33
33
|
return runOrDiagnose(args, { account });
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
server.registerTool('gog_docs_trash', {
|
|
37
|
+
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.',
|
|
38
|
+
annotations: { destructiveHint: true },
|
|
39
|
+
inputSchema: {
|
|
40
|
+
docId: z.string().describe('Doc ID to move to trash'),
|
|
41
|
+
account: accountParam,
|
|
42
|
+
},
|
|
43
|
+
}, async ({ docId, account }) => {
|
|
44
|
+
return runOrDiagnose(['drive', 'delete', docId], { account });
|
|
45
|
+
});
|
|
46
|
+
|
|
36
47
|
server.registerTool('gog_docs_edit', {
|
|
37
48
|
description: 'Edit a Google Doc by finding and replacing text (stream-edit style).',
|
|
38
49
|
annotations: { destructiveHint: true },
|
|
@@ -49,6 +60,94 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
49
60
|
return runOrDiagnose(args, { account });
|
|
50
61
|
});
|
|
51
62
|
|
|
63
|
+
server.registerTool('gog_docs_read', {
|
|
64
|
+
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.',
|
|
65
|
+
annotations: { readOnlyHint: true },
|
|
66
|
+
inputSchema: {
|
|
67
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
68
|
+
format: z.enum(['text', 'json']).optional().describe('Output format (default: text)'),
|
|
69
|
+
tab: z.string().optional().describe('Target tab title or ID (text mode only)'),
|
|
70
|
+
allTabs: z.boolean().optional().describe('Show all tabs with headers (text mode only)'),
|
|
71
|
+
maxBytes: z.number().optional().describe('Max bytes to read in text mode (0 = unlimited; default 2000000)'),
|
|
72
|
+
account: accountParam,
|
|
73
|
+
},
|
|
74
|
+
}, async ({ docId, format, tab, allTabs, maxBytes, account }) => {
|
|
75
|
+
if (format === 'json') {
|
|
76
|
+
return runOrDiagnose(['docs', 'raw', docId, '--pretty'], { account });
|
|
77
|
+
}
|
|
78
|
+
const args = ['docs', 'cat', docId];
|
|
79
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
80
|
+
if (allTabs) args.push('--all-tabs');
|
|
81
|
+
if (maxBytes !== undefined) args.push(`--max-bytes=${maxBytes}`);
|
|
82
|
+
return runOrDiagnose(args, { account });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
server.registerTool('gog_docs_format', {
|
|
86
|
+
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.',
|
|
87
|
+
annotations: { destructiveHint: true },
|
|
88
|
+
inputSchema: {
|
|
89
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
90
|
+
match: z.string().optional().describe('Format only the first text match'),
|
|
91
|
+
matchAll: z.boolean().optional().describe('Format all matches instead of only the first'),
|
|
92
|
+
matchCase: z.boolean().optional().describe('Case-sensitive matching'),
|
|
93
|
+
tab: z.string().optional().describe('Target tab title or ID'),
|
|
94
|
+
fontFamily: z.string().optional().describe('Font family (e.g. Arial, Georgia)'),
|
|
95
|
+
fontSize: z.number().optional().describe('Font size in points'),
|
|
96
|
+
textColor: z.string().optional().describe('Text color as #RRGGBB or #RGB'),
|
|
97
|
+
bgColor: z.string().optional().describe('Text background color as #RRGGBB or #RGB'),
|
|
98
|
+
bold: z.boolean().optional().describe('Set bold'),
|
|
99
|
+
noBold: z.boolean().optional().describe('Clear bold'),
|
|
100
|
+
italic: z.boolean().optional().describe('Set italic'),
|
|
101
|
+
noItalic: z.boolean().optional().describe('Clear italic'),
|
|
102
|
+
underline: z.boolean().optional().describe('Set underline'),
|
|
103
|
+
noUnderline: z.boolean().optional().describe('Clear underline'),
|
|
104
|
+
strikethrough: z.boolean().optional().describe('Set strikethrough'),
|
|
105
|
+
noStrikethrough: z.boolean().optional().describe('Clear strikethrough'),
|
|
106
|
+
alignment: z.enum(['left', 'center', 'right', 'justify', 'start', 'end', 'justified']).optional().describe('Paragraph alignment'),
|
|
107
|
+
lineSpacing: z.number().optional().describe('Line spacing percentage (e.g. 100 for single, 150 for 1.5x, 200 for double)'),
|
|
108
|
+
account: accountParam,
|
|
109
|
+
},
|
|
110
|
+
}, async (args) => {
|
|
111
|
+
const a = args as {
|
|
112
|
+
docId: string;
|
|
113
|
+
match?: string;
|
|
114
|
+
matchAll?: boolean;
|
|
115
|
+
matchCase?: boolean;
|
|
116
|
+
tab?: string;
|
|
117
|
+
fontFamily?: string;
|
|
118
|
+
fontSize?: number;
|
|
119
|
+
textColor?: string;
|
|
120
|
+
bgColor?: string;
|
|
121
|
+
bold?: boolean; noBold?: boolean;
|
|
122
|
+
italic?: boolean; noItalic?: boolean;
|
|
123
|
+
underline?: boolean; noUnderline?: boolean;
|
|
124
|
+
strikethrough?: boolean; noStrikethrough?: boolean;
|
|
125
|
+
alignment?: string;
|
|
126
|
+
lineSpacing?: number;
|
|
127
|
+
account?: string;
|
|
128
|
+
};
|
|
129
|
+
const argv = ['docs', 'format', a.docId];
|
|
130
|
+
if (a.match) argv.push(`--match=${a.match}`);
|
|
131
|
+
if (a.matchAll) argv.push('--match-all');
|
|
132
|
+
if (a.matchCase) argv.push('--match-case');
|
|
133
|
+
if (a.tab) argv.push(`--tab=${a.tab}`);
|
|
134
|
+
if (a.fontFamily) argv.push(`--font-family=${a.fontFamily}`);
|
|
135
|
+
if (a.fontSize !== undefined) argv.push(`--font-size=${a.fontSize}`);
|
|
136
|
+
if (a.textColor) argv.push(`--text-color=${a.textColor}`);
|
|
137
|
+
if (a.bgColor) argv.push(`--bg-color=${a.bgColor}`);
|
|
138
|
+
if (a.bold) argv.push('--bold');
|
|
139
|
+
if (a.noBold) argv.push('--no-bold');
|
|
140
|
+
if (a.italic) argv.push('--italic');
|
|
141
|
+
if (a.noItalic) argv.push('--no-italic');
|
|
142
|
+
if (a.underline) argv.push('--underline');
|
|
143
|
+
if (a.noUnderline) argv.push('--no-underline');
|
|
144
|
+
if (a.strikethrough) argv.push('--strikethrough');
|
|
145
|
+
if (a.noStrikethrough) argv.push('--no-strikethrough');
|
|
146
|
+
if (a.alignment) argv.push(`--alignment=${a.alignment}`);
|
|
147
|
+
if (a.lineSpacing !== undefined) argv.push(`--line-spacing=${a.lineSpacing}`);
|
|
148
|
+
return runOrDiagnose(argv, { account: a.account });
|
|
149
|
+
});
|
|
150
|
+
|
|
52
151
|
server.registerTool('gog_docs_export', {
|
|
53
152
|
description: 'Export a Google Doc as PDF, plain text, HTML, DOCX, or other format.',
|
|
54
153
|
annotations: { readOnlyHint: true },
|
|
@@ -66,12 +165,12 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
66
165
|
});
|
|
67
166
|
|
|
68
167
|
server.registerTool('gog_docs_insert', {
|
|
69
|
-
description: 'Insert text at a specific
|
|
168
|
+
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.',
|
|
70
169
|
annotations: { destructiveHint: true },
|
|
71
170
|
inputSchema: {
|
|
72
171
|
docId: z.string().describe('Doc ID (from the URL)'),
|
|
73
172
|
content: z.string().optional().describe('Text content to insert'),
|
|
74
|
-
index: z.number().optional().describe('Character index to insert at (default:
|
|
173
|
+
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.'),
|
|
75
174
|
file: z.string().optional().describe('Path to a file whose content to insert'),
|
|
76
175
|
tabId: z.string().optional().describe('Tab ID to insert into (for multi-tab docs)'),
|
|
77
176
|
account: accountParam,
|
|
@@ -85,6 +184,26 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
85
184
|
return runOrDiagnose(args, { account });
|
|
86
185
|
});
|
|
87
186
|
|
|
187
|
+
server.registerTool('gog_docs_append', {
|
|
188
|
+
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.',
|
|
189
|
+
annotations: { destructiveHint: true },
|
|
190
|
+
inputSchema: {
|
|
191
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
192
|
+
text: z.string().optional().describe('Text content to append'),
|
|
193
|
+
file: z.string().optional().describe('Path to a text file to append (use "-" for stdin)'),
|
|
194
|
+
markdown: z.boolean().optional().describe('Convert markdown to Google Docs formatting (headings, bold, lists, etc.)'),
|
|
195
|
+
tab: z.string().optional().describe('Target tab title or ID (for multi-tab docs)'),
|
|
196
|
+
account: accountParam,
|
|
197
|
+
},
|
|
198
|
+
}, async ({ docId, text, file, markdown, tab, account }) => {
|
|
199
|
+
const args = ['docs', 'write', docId, '--append'];
|
|
200
|
+
if (text) args.push(`--text=${text}`);
|
|
201
|
+
if (file) args.push(`--file=${file}`);
|
|
202
|
+
if (markdown) args.push('--markdown');
|
|
203
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
204
|
+
return runOrDiagnose(args, { account });
|
|
205
|
+
});
|
|
206
|
+
|
|
88
207
|
server.registerTool('gog_docs_list_tabs', {
|
|
89
208
|
description: 'List all tabs in a Google Doc.',
|
|
90
209
|
annotations: { readOnlyHint: true },
|
|
@@ -518,3 +518,129 @@ describe('gog_docs_comments_delete', () => {
|
|
|
518
518
|
);
|
|
519
519
|
});
|
|
520
520
|
});
|
|
521
|
+
|
|
522
|
+
describe('gog_docs_trash', () => {
|
|
523
|
+
it('routes to gog drive delete <docId>', async () => {
|
|
524
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
525
|
+
const handlers = setupHandlers();
|
|
526
|
+
await handlers.get('gog_docs_trash')!({ docId: 'd1' });
|
|
527
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'delete', 'd1'], { account: undefined });
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('gog_docs_append', () => {
|
|
532
|
+
it('uses gog docs write --append', async () => {
|
|
533
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
534
|
+
const handlers = setupHandlers();
|
|
535
|
+
await handlers.get('gog_docs_append')!({ docId: 'd1', text: 'Hello' });
|
|
536
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
537
|
+
['docs', 'write', 'd1', '--append', '--text=Hello'],
|
|
538
|
+
{ account: undefined },
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('passes file, markdown, and tab flags', async () => {
|
|
543
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
544
|
+
const handlers = setupHandlers();
|
|
545
|
+
await handlers.get('gog_docs_append')!({
|
|
546
|
+
docId: 'd1', file: '/tmp/section.md', markdown: true, tab: 'Notes',
|
|
547
|
+
});
|
|
548
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
549
|
+
['docs', 'write', 'd1', '--append', '--file=/tmp/section.md', '--markdown', '--tab=Notes'],
|
|
550
|
+
{ account: undefined },
|
|
551
|
+
);
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
describe('gog_docs_read', () => {
|
|
556
|
+
it('defaults to plain text via gog docs cat', async () => {
|
|
557
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('hello'));
|
|
558
|
+
const handlers = setupHandlers();
|
|
559
|
+
await handlers.get('gog_docs_read')!({ docId: 'd1' });
|
|
560
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'cat', 'd1'], { account: undefined });
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('routes json format to gog docs raw --pretty', async () => {
|
|
564
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
565
|
+
const handlers = setupHandlers();
|
|
566
|
+
await handlers.get('gog_docs_read')!({ docId: 'd1', format: 'json' });
|
|
567
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'raw', 'd1', '--pretty'], { account: undefined });
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('passes tab, allTabs, maxBytes in text mode', async () => {
|
|
571
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText(''));
|
|
572
|
+
const handlers = setupHandlers();
|
|
573
|
+
await handlers.get('gog_docs_read')!({ docId: 'd1', tab: 'Section A', allTabs: true, maxBytes: 0 });
|
|
574
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
575
|
+
['docs', 'cat', 'd1', '--tab=Section A', '--all-tabs', '--max-bytes=0'],
|
|
576
|
+
{ account: undefined },
|
|
577
|
+
);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
describe('gog_docs_format', () => {
|
|
582
|
+
it('passes all text/paragraph attribute flags', async () => {
|
|
583
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
584
|
+
const handlers = setupHandlers();
|
|
585
|
+
await handlers.get('gog_docs_format')!({
|
|
586
|
+
docId: 'd1',
|
|
587
|
+
match: 'Title',
|
|
588
|
+
matchAll: true,
|
|
589
|
+
matchCase: true,
|
|
590
|
+
tab: 'Body',
|
|
591
|
+
fontFamily: 'Arial',
|
|
592
|
+
fontSize: 18,
|
|
593
|
+
textColor: '#333333',
|
|
594
|
+
bgColor: '#FFF5D9',
|
|
595
|
+
bold: true,
|
|
596
|
+
italic: true,
|
|
597
|
+
underline: true,
|
|
598
|
+
strikethrough: true,
|
|
599
|
+
alignment: 'center',
|
|
600
|
+
lineSpacing: 150,
|
|
601
|
+
});
|
|
602
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
603
|
+
[
|
|
604
|
+
'docs', 'format', 'd1',
|
|
605
|
+
'--match=Title',
|
|
606
|
+
'--match-all',
|
|
607
|
+
'--match-case',
|
|
608
|
+
'--tab=Body',
|
|
609
|
+
'--font-family=Arial',
|
|
610
|
+
'--font-size=18',
|
|
611
|
+
'--text-color=#333333',
|
|
612
|
+
'--bg-color=#FFF5D9',
|
|
613
|
+
'--bold',
|
|
614
|
+
'--italic',
|
|
615
|
+
'--underline',
|
|
616
|
+
'--strikethrough',
|
|
617
|
+
'--alignment=center',
|
|
618
|
+
'--line-spacing=150',
|
|
619
|
+
],
|
|
620
|
+
{ account: undefined },
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('emits clear-style flags (noBold/noItalic/...) without the set variants', async () => {
|
|
625
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
626
|
+
const handlers = setupHandlers();
|
|
627
|
+
await handlers.get('gog_docs_format')!({
|
|
628
|
+
docId: 'd1',
|
|
629
|
+
noBold: true,
|
|
630
|
+
noItalic: true,
|
|
631
|
+
noUnderline: true,
|
|
632
|
+
noStrikethrough: true,
|
|
633
|
+
});
|
|
634
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
635
|
+
['docs', 'format', 'd1', '--no-bold', '--no-italic', '--no-underline', '--no-strikethrough'],
|
|
636
|
+
{ account: undefined },
|
|
637
|
+
);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('omits all flags when not provided (whole-doc no-op call passes through)', async () => {
|
|
641
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
642
|
+
const handlers = setupHandlers();
|
|
643
|
+
await handlers.get('gog_docs_format')!({ docId: 'd1' });
|
|
644
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'format', 'd1'], { account: undefined });
|
|
645
|
+
});
|
|
646
|
+
});
|