gogcli-mcp-docs 2.0.10 → 2.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -3
- package/SKILL.md +14 -2
- package/dist/index.js +70 -4
- package/manifest.json +19 -3
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/tools/docs-extra.ts +74 -2
- package/tests/tools/docs-extra.test.ts +155 -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.12"
|
|
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.12",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
> [!WARNING]
|
|
4
4
|
> **AI-developed project.** This codebase was built and is actively maintained by [Claude Code](https://www.anthropic.com/claude). Review all code and tool permissions before use.
|
|
5
5
|
|
|
6
|
-
Extended Google Docs [MCP](https://modelcontextprotocol.io) server via [gogcli](https://github.com/
|
|
6
|
+
Extended Google Docs [MCP](https://modelcontextprotocol.io) server via [gogcli](https://github.com/openclaw/gogcli). Includes auth tools plus 18 additional dedicated Docs tools for editing, exporting, comments, and more.
|
|
7
7
|
|
|
8
8
|
## Requirements
|
|
9
9
|
|
|
10
|
-
- [gogcli](https://github.com/
|
|
10
|
+
- [gogcli](https://github.com/openclaw/gogcli) installed and authenticated
|
|
11
11
|
- Node.js 18+
|
|
12
12
|
|
|
13
13
|
```bash
|
|
@@ -44,19 +44,23 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
44
44
|
claude mcp add gogcli-docs -- gogcli-mcp-docs
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
## Extra Docs Tools (
|
|
47
|
+
## Extra Docs Tools (18)
|
|
48
48
|
|
|
49
49
|
Plus 5 auth tools and 7 base Docs tools (info, cat, create, write, find-replace, structure, run).
|
|
50
50
|
|
|
51
51
|
| Tool | Description |
|
|
52
52
|
|------|-------------|
|
|
53
|
+
| `gog_docs_append` | Append text or markdown to the end of a doc |
|
|
53
54
|
| `gog_docs_copy` | Copy a document |
|
|
54
55
|
| `gog_docs_delete` | Delete content by character index range |
|
|
55
56
|
| `gog_docs_edit` | Find and replace with case-sensitivity control |
|
|
56
57
|
| `gog_docs_export` | Export as PDF, TXT, HTML, DOCX, RTF, ODT, or EPUB |
|
|
58
|
+
| `gog_docs_format` | Apply character / paragraph formatting (bold, color, alignment, …) |
|
|
57
59
|
| `gog_docs_insert` | Insert text at a specific position |
|
|
58
60
|
| `gog_docs_list_tabs` | List all document tabs |
|
|
61
|
+
| `gog_docs_read` | Read doc as plain text or raw JSON |
|
|
59
62
|
| `gog_docs_sed` | Stream-edit with sed-like regex expressions |
|
|
63
|
+
| `gog_docs_trash` | Move a doc to Drive trash |
|
|
60
64
|
| `gog_docs_update` | Update document content at a specific position |
|
|
61
65
|
| `gog_docs_comments_list` | List comments (open or resolved) |
|
|
62
66
|
| `gog_docs_comments_get` | Get a comment with its replies |
|
|
@@ -65,6 +69,16 @@ Plus 5 auth tools and 7 base Docs tools (info, cat, create, write, find-replace,
|
|
|
65
69
|
| `gog_docs_comments_resolve` | Resolve a comment |
|
|
66
70
|
| `gog_docs_comments_delete` | Delete a comment |
|
|
67
71
|
|
|
72
|
+
## Known limitations: `gog_docs_append` with `markdown: true`
|
|
73
|
+
|
|
74
|
+
These bugs live in gogcli's upstream markdown → Docs converter; the wrapper just passes `--markdown` through. Tracked upstream:
|
|
75
|
+
|
|
76
|
+
| Symptom | Workaround | Upstream |
|
|
77
|
+
|---------|-----------|----------|
|
|
78
|
+
| 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](https://github.com/openclaw/gogcli/issues/607) |
|
|
79
|
+
| Inline `**bold**` / `*italic*` / `` `code` `` inside table cells renders as literal characters | Insert plain cell text, then apply formatting via `gog_docs_format` | [openclaw/gogcli#608](https://github.com/openclaw/gogcli/issues/608) |
|
|
80
|
+
| Tables with an empty header row leak the last data row as literal pipe text | Always supply a non-empty header row | [openclaw/gogcli#609](https://github.com/openclaw/gogcli/issues/609) |
|
|
81
|
+
|
|
68
82
|
## License
|
|
69
83
|
|
|
70
84
|
MIT
|
package/SKILL.md
CHANGED
|
@@ -5,13 +5,13 @@ description: Use when the user asks to read, write, edit, export, or comment on
|
|
|
5
5
|
|
|
6
6
|
# gogcli-mcp-docs
|
|
7
7
|
|
|
8
|
-
Extended Google Docs MCP server via [gogcli](https://github.com/
|
|
8
|
+
Extended Google Docs MCP server via [gogcli](https://github.com/openclaw/gogcli) — 30 tools: auth + 7 base Docs + 18 extra dedicated Docs tools.
|
|
9
9
|
|
|
10
10
|
- **Source:** [github.com/chrischall/gogcli-mcp](https://github.com/chrischall/gogcli-mcp)
|
|
11
11
|
|
|
12
12
|
## Requirements
|
|
13
13
|
|
|
14
|
-
- [gogcli](https://github.com/
|
|
14
|
+
- [gogcli](https://github.com/openclaw/gogcli) installed and authenticated
|
|
15
15
|
- Node.js 18 or later
|
|
16
16
|
|
|
17
17
|
## Setup
|
|
@@ -34,13 +34,17 @@ Extended Google Docs MCP server via [gogcli](https://github.com/steipete/gogcli)
|
|
|
34
34
|
|
|
35
35
|
| Tool | What it does |
|
|
36
36
|
|------|-------------|
|
|
37
|
+
| `gog_docs_append` | Append text or markdown to the end of a doc |
|
|
37
38
|
| `gog_docs_copy` | Copy a document |
|
|
38
39
|
| `gog_docs_delete` | Delete content by character index range |
|
|
39
40
|
| `gog_docs_edit` | Find and replace with case-sensitivity |
|
|
40
41
|
| `gog_docs_export` | Export as PDF, TXT, HTML, DOCX, RTF, ODT, or EPUB |
|
|
42
|
+
| `gog_docs_format` | Apply character / paragraph formatting |
|
|
41
43
|
| `gog_docs_insert` | Insert text at a specific position |
|
|
42
44
|
| `gog_docs_list_tabs` | List all document tabs |
|
|
45
|
+
| `gog_docs_read` | Read doc as plain text or raw JSON |
|
|
43
46
|
| `gog_docs_sed` | Stream-edit with sed-like regex |
|
|
47
|
+
| `gog_docs_trash` | Move a doc to Drive trash |
|
|
44
48
|
| `gog_docs_update` | Update content at a specific position |
|
|
45
49
|
| `gog_docs_comments_list` | List comments (open or resolved) |
|
|
46
50
|
| `gog_docs_comments_get` | Get a comment with replies |
|
|
@@ -50,3 +54,11 @@ Extended Google Docs MCP server via [gogcli](https://github.com/steipete/gogcli)
|
|
|
50
54
|
| `gog_docs_comments_delete` | Delete a comment |
|
|
51
55
|
|
|
52
56
|
Plus 5 auth tools and 7 base Docs tools.
|
|
57
|
+
|
|
58
|
+
### `gog_docs_append` with `markdown: true` — known limitations
|
|
59
|
+
|
|
60
|
+
Three bugs in gogcli's upstream markdown→Docs converter to be aware of:
|
|
61
|
+
|
|
62
|
+
- **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](https://github.com/openclaw/gogcli/issues/607)).
|
|
63
|
+
- **Inline `**bold**` / `*italic*` / `` `code` `` inside table cells** renders as literal characters — insert plain cell text, then apply formatting via `gog_docs_format` ([openclaw/gogcli#608](https://github.com/openclaw/gogcli/issues/608)).
|
|
64
|
+
- **Empty-header tables** leak the last data row as literal pipe text — always supply a non-empty header row ([openclaw/gogcli#609](https://github.com/openclaw/gogcli/issues/609)).
|
package/dist/index.js
CHANGED
|
@@ -31063,7 +31063,7 @@ async function run(args, options = {}) {
|
|
|
31063
31063
|
settled = true;
|
|
31064
31064
|
if (err.code === "ENOENT") {
|
|
31065
31065
|
reject(new Error(
|
|
31066
|
-
"gog executable not found. Install gogcli (https://github.com/
|
|
31066
|
+
"gog executable not found. Install gogcli (https://github.com/openclaw/gogcli) or set GOG_PATH in your MCP client config to the absolute binary path (run `which gog` in a terminal to find it)."
|
|
31067
31067
|
));
|
|
31068
31068
|
return;
|
|
31069
31069
|
}
|
|
@@ -31292,7 +31292,7 @@ function registerDocsTools(server2) {
|
|
|
31292
31292
|
var cellValueParam = external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]);
|
|
31293
31293
|
|
|
31294
31294
|
// ../gogcli-mcp/src/server.ts
|
|
31295
|
-
var VERSION = true ? "2.0.
|
|
31295
|
+
var VERSION = true ? "2.0.12" : "0.0.0";
|
|
31296
31296
|
function createServer(options) {
|
|
31297
31297
|
return new McpServer({
|
|
31298
31298
|
name: options?.name ?? "gogcli",
|
|
@@ -31399,6 +31399,8 @@ function registerExtraDocsTools(server2) {
|
|
|
31399
31399
|
noStrikethrough: external_exports.boolean().optional().describe("Clear strikethrough"),
|
|
31400
31400
|
alignment: external_exports.enum(["left", "center", "right", "justify", "start", "end", "justified"]).optional().describe("Paragraph alignment"),
|
|
31401
31401
|
lineSpacing: external_exports.number().optional().describe("Line spacing percentage (e.g. 100 for single, 150 for 1.5x, 200 for double)"),
|
|
31402
|
+
headingLevel: external_exports.number().int().optional().describe("Set paragraph named style to HEADING_1..HEADING_6 (shortcut for namedStyle=HEADING_N)"),
|
|
31403
|
+
namedStyle: external_exports.enum(["NORMAL_TEXT", "TITLE", "SUBTITLE", "HEADING_1", "HEADING_2", "HEADING_3", "HEADING_4", "HEADING_5", "HEADING_6"]).optional().describe("Set paragraph named style explicitly"),
|
|
31402
31404
|
account: accountParam
|
|
31403
31405
|
}
|
|
31404
31406
|
}, async (args) => {
|
|
@@ -31422,6 +31424,8 @@ function registerExtraDocsTools(server2) {
|
|
|
31422
31424
|
if (a.noStrikethrough) argv.push("--no-strikethrough");
|
|
31423
31425
|
if (a.alignment) argv.push(`--alignment=${a.alignment}`);
|
|
31424
31426
|
if (a.lineSpacing !== void 0) argv.push(`--line-spacing=${a.lineSpacing}`);
|
|
31427
|
+
if (a.headingLevel !== void 0) argv.push(`--heading-level=${a.headingLevel}`);
|
|
31428
|
+
if (a.namedStyle) argv.push(`--named-style=${a.namedStyle}`);
|
|
31425
31429
|
return runOrDiagnose(argv, { account: a.account });
|
|
31426
31430
|
});
|
|
31427
31431
|
server2.registerTool("gog_docs_export", {
|
|
@@ -31459,13 +31463,13 @@ function registerExtraDocsTools(server2) {
|
|
|
31459
31463
|
return runOrDiagnose(args, { account });
|
|
31460
31464
|
});
|
|
31461
31465
|
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.",
|
|
31466
|
+
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. Known markdown=true limitations (tracked upstream): (a) 3+ tables in one call reorders the trailing punctuation of the paragraph before the 3rd table \u2014 split into multiple calls with \u22642 tables each (openclaw/gogcli#607); (b) inline **bold** / *italic* / `code` inside table cells renders as literal characters \u2014 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 \u2014 always supply a non-empty header (openclaw/gogcli#609).",
|
|
31463
31467
|
annotations: { destructiveHint: true },
|
|
31464
31468
|
inputSchema: {
|
|
31465
31469
|
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31466
31470
|
text: external_exports.string().optional().describe("Text content to append"),
|
|
31467
31471
|
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.)"),
|
|
31472
|
+
markdown: external_exports.boolean().optional().describe("Convert markdown to Google Docs formatting (headings, bold, lists, etc.). See the tool description for known upstream limitations around tables."),
|
|
31469
31473
|
tab: external_exports.string().optional().describe("Target tab title or ID (for multi-tab docs)"),
|
|
31470
31474
|
account: accountParam
|
|
31471
31475
|
}
|
|
@@ -31604,6 +31608,68 @@ function registerExtraDocsTools(server2) {
|
|
|
31604
31608
|
}, async ({ docId, commentId, account }) => {
|
|
31605
31609
|
return runOrDiagnose(["docs", "comments", "delete", docId, commentId], { account });
|
|
31606
31610
|
});
|
|
31611
|
+
server2.registerTool("gog_docs_comments_reopen", {
|
|
31612
|
+
description: "Reopen a previously resolved comment (flip resolved \u2192 open).",
|
|
31613
|
+
annotations: { destructiveHint: true },
|
|
31614
|
+
inputSchema: {
|
|
31615
|
+
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31616
|
+
commentId: external_exports.string().describe("Comment ID to reopen"),
|
|
31617
|
+
account: accountParam
|
|
31618
|
+
}
|
|
31619
|
+
}, async ({ docId, commentId, account }) => {
|
|
31620
|
+
return runOrDiagnose(["docs", "comments", "reopen", docId, commentId], { account });
|
|
31621
|
+
});
|
|
31622
|
+
server2.registerTool("gog_docs_insert_page_break", {
|
|
31623
|
+
description: "Insert a Google Docs page break via InsertPageBreakRequest \u2014 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.",
|
|
31624
|
+
annotations: { destructiveHint: true },
|
|
31625
|
+
inputSchema: {
|
|
31626
|
+
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31627
|
+
index: external_exports.number().int().optional().describe("Character index to insert at (1 = beginning). Omit or use atEnd for end-of-doc."),
|
|
31628
|
+
atEnd: external_exports.boolean().optional().describe("Insert at end-of-doc/tab (mutually exclusive with index)"),
|
|
31629
|
+
tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
|
|
31630
|
+
account: accountParam
|
|
31631
|
+
}
|
|
31632
|
+
}, async ({ docId, index, atEnd, tab, account }) => {
|
|
31633
|
+
const args = ["docs", "insert-page-break", docId];
|
|
31634
|
+
if (index !== void 0) args.push(`--index=${index}`);
|
|
31635
|
+
if (atEnd) args.push("--at-end");
|
|
31636
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
31637
|
+
return runOrDiagnose(args, { account });
|
|
31638
|
+
});
|
|
31639
|
+
server2.registerTool("gog_docs_page_layout", {
|
|
31640
|
+
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.",
|
|
31641
|
+
annotations: { destructiveHint: true },
|
|
31642
|
+
inputSchema: {
|
|
31643
|
+
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31644
|
+
layout: external_exports.enum(["pageless", "pages"]).optional().describe("Page layout (default: pageless)"),
|
|
31645
|
+
account: accountParam
|
|
31646
|
+
}
|
|
31647
|
+
}, async ({ docId, layout, account }) => {
|
|
31648
|
+
const args = ["docs", "page-layout", docId];
|
|
31649
|
+
if (layout) args.push(`--layout=${layout}`);
|
|
31650
|
+
return runOrDiagnose(args, { account });
|
|
31651
|
+
});
|
|
31652
|
+
server2.registerTool("gog_docs_insert_table", {
|
|
31653
|
+
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.",
|
|
31654
|
+
annotations: { destructiveHint: true },
|
|
31655
|
+
inputSchema: {
|
|
31656
|
+
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31657
|
+
rows: external_exports.number().int().min(1).describe("Number of rows (>=1)"),
|
|
31658
|
+
cols: external_exports.number().int().min(1).describe("Number of columns (>=1)"),
|
|
31659
|
+
index: external_exports.number().int().optional().describe("Character index to insert at (1 = beginning). Omit or use atEnd for end-of-doc."),
|
|
31660
|
+
atEnd: external_exports.boolean().optional().describe("Insert at end-of-doc/tab (mutually exclusive with index)"),
|
|
31661
|
+
valuesJson: external_exports.string().optional().describe("Cell values as a JSON 2D string array; dimensions must match rows x cols when supplied"),
|
|
31662
|
+
tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
|
|
31663
|
+
account: accountParam
|
|
31664
|
+
}
|
|
31665
|
+
}, async ({ docId, rows, cols, index, atEnd, valuesJson, tab, account }) => {
|
|
31666
|
+
const args = ["docs", "insert-table", docId, `--rows=${rows}`, `--cols=${cols}`];
|
|
31667
|
+
if (index !== void 0) args.push(`--index=${index}`);
|
|
31668
|
+
if (atEnd) args.push("--at-end");
|
|
31669
|
+
if (valuesJson !== void 0) args.push(`--values-json=${valuesJson}`);
|
|
31670
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
31671
|
+
return runOrDiagnose(args, { account });
|
|
31672
|
+
});
|
|
31607
31673
|
}
|
|
31608
31674
|
|
|
31609
31675
|
// src/index.ts
|
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.12",
|
|
7
7
|
"description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
"name": "gog_docs_append",
|
|
164
|
-
"description": "Append text to the end of a Google Doc (the right tool for iterative construction
|
|
164
|
+
"description": "Append text to the end of a Google Doc (the right tool for iterative construction). markdown=true known limitations (upstream): 3+ tables/call reorders trailing punctuation (split calls); inline bold/italic/code in cells renders literally (format after); empty-header tables leak literal pipes (use non-empty header)."
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
"name": "gog_docs_read",
|
|
@@ -169,11 +169,27 @@
|
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
"name": "gog_docs_format",
|
|
172
|
-
"description": "Apply text/paragraph formatting (bold, italic, font size, color, alignment, line spacing) — supports match, matchAll, matchCase"
|
|
172
|
+
"description": "Apply text/paragraph formatting (bold, italic, font size, color, alignment, line spacing, named style / heading level) — supports match, matchAll, matchCase"
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
"name": "gog_docs_trash",
|
|
176
176
|
"description": "Move an entire Google Doc to Drive trash (convenience wrapper around gog drive delete)"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"name": "gog_docs_comments_reopen",
|
|
180
|
+
"description": "Reopen a previously resolved comment."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "gog_docs_insert_page_break",
|
|
184
|
+
"description": "Insert a Google Docs page break at a given index or at end-of-doc — the only path for multi-page deliverables."
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "gog_docs_page_layout",
|
|
188
|
+
"description": "Toggle page layout (pageless | pages) on an existing Google Doc."
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "gog_docs_insert_table",
|
|
192
|
+
"description": "Insert a native Google Docs table with given dimensions, optionally populated from a JSON 2D array."
|
|
177
193
|
}
|
|
178
194
|
],
|
|
179
195
|
"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.12",
|
|
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.12",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp-docs",
|
|
15
|
-
"version": "2.0.
|
|
15
|
+
"version": "2.0.12",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/src/tools/docs-extra.ts
CHANGED
|
@@ -105,6 +105,8 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
105
105
|
noStrikethrough: z.boolean().optional().describe('Clear strikethrough'),
|
|
106
106
|
alignment: z.enum(['left', 'center', 'right', 'justify', 'start', 'end', 'justified']).optional().describe('Paragraph alignment'),
|
|
107
107
|
lineSpacing: z.number().optional().describe('Line spacing percentage (e.g. 100 for single, 150 for 1.5x, 200 for double)'),
|
|
108
|
+
headingLevel: z.number().int().optional().describe('Set paragraph named style to HEADING_1..HEADING_6 (shortcut for namedStyle=HEADING_N)'),
|
|
109
|
+
namedStyle: z.enum(['NORMAL_TEXT', 'TITLE', 'SUBTITLE', 'HEADING_1', 'HEADING_2', 'HEADING_3', 'HEADING_4', 'HEADING_5', 'HEADING_6']).optional().describe('Set paragraph named style explicitly'),
|
|
108
110
|
account: accountParam,
|
|
109
111
|
},
|
|
110
112
|
}, async (args) => {
|
|
@@ -124,6 +126,8 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
124
126
|
strikethrough?: boolean; noStrikethrough?: boolean;
|
|
125
127
|
alignment?: string;
|
|
126
128
|
lineSpacing?: number;
|
|
129
|
+
headingLevel?: number;
|
|
130
|
+
namedStyle?: string;
|
|
127
131
|
account?: string;
|
|
128
132
|
};
|
|
129
133
|
const argv = ['docs', 'format', a.docId];
|
|
@@ -145,6 +149,8 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
145
149
|
if (a.noStrikethrough) argv.push('--no-strikethrough');
|
|
146
150
|
if (a.alignment) argv.push(`--alignment=${a.alignment}`);
|
|
147
151
|
if (a.lineSpacing !== undefined) argv.push(`--line-spacing=${a.lineSpacing}`);
|
|
152
|
+
if (a.headingLevel !== undefined) argv.push(`--heading-level=${a.headingLevel}`);
|
|
153
|
+
if (a.namedStyle) argv.push(`--named-style=${a.namedStyle}`);
|
|
148
154
|
return runOrDiagnose(argv, { account: a.account });
|
|
149
155
|
});
|
|
150
156
|
|
|
@@ -185,13 +191,13 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
185
191
|
});
|
|
186
192
|
|
|
187
193
|
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.',
|
|
194
|
+
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).',
|
|
189
195
|
annotations: { destructiveHint: true },
|
|
190
196
|
inputSchema: {
|
|
191
197
|
docId: z.string().describe('Doc ID (from the URL)'),
|
|
192
198
|
text: z.string().optional().describe('Text content to append'),
|
|
193
199
|
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.)'),
|
|
200
|
+
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.'),
|
|
195
201
|
tab: z.string().optional().describe('Target tab title or ID (for multi-tab docs)'),
|
|
196
202
|
account: accountParam,
|
|
197
203
|
},
|
|
@@ -342,4 +348,70 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
342
348
|
}, async ({ docId, commentId, account }) => {
|
|
343
349
|
return runOrDiagnose(['docs', 'comments', 'delete', docId, commentId], { account });
|
|
344
350
|
});
|
|
351
|
+
|
|
352
|
+
server.registerTool('gog_docs_comments_reopen', {
|
|
353
|
+
description: 'Reopen a previously resolved comment (flip resolved → open).',
|
|
354
|
+
annotations: { destructiveHint: true },
|
|
355
|
+
inputSchema: {
|
|
356
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
357
|
+
commentId: z.string().describe('Comment ID to reopen'),
|
|
358
|
+
account: accountParam,
|
|
359
|
+
},
|
|
360
|
+
}, async ({ docId, commentId, account }) => {
|
|
361
|
+
return runOrDiagnose(['docs', 'comments', 'reopen', docId, commentId], { account });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
server.registerTool('gog_docs_insert_page_break', {
|
|
365
|
+
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.',
|
|
366
|
+
annotations: { destructiveHint: true },
|
|
367
|
+
inputSchema: {
|
|
368
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
369
|
+
index: z.number().int().optional().describe('Character index to insert at (1 = beginning). Omit or use atEnd for end-of-doc.'),
|
|
370
|
+
atEnd: z.boolean().optional().describe('Insert at end-of-doc/tab (mutually exclusive with index)'),
|
|
371
|
+
tab: z.string().optional().describe('Target a specific tab by title or ID'),
|
|
372
|
+
account: accountParam,
|
|
373
|
+
},
|
|
374
|
+
}, async ({ docId, index, atEnd, tab, account }) => {
|
|
375
|
+
const args = ['docs', 'insert-page-break', docId];
|
|
376
|
+
if (index !== undefined) args.push(`--index=${index}`);
|
|
377
|
+
if (atEnd) args.push('--at-end');
|
|
378
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
379
|
+
return runOrDiagnose(args, { account });
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
server.registerTool('gog_docs_page_layout', {
|
|
383
|
+
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.',
|
|
384
|
+
annotations: { destructiveHint: true },
|
|
385
|
+
inputSchema: {
|
|
386
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
387
|
+
layout: z.enum(['pageless', 'pages']).optional().describe('Page layout (default: pageless)'),
|
|
388
|
+
account: accountParam,
|
|
389
|
+
},
|
|
390
|
+
}, async ({ docId, layout, account }) => {
|
|
391
|
+
const args = ['docs', 'page-layout', docId];
|
|
392
|
+
if (layout) args.push(`--layout=${layout}`);
|
|
393
|
+
return runOrDiagnose(args, { account });
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
server.registerTool('gog_docs_insert_table', {
|
|
397
|
+
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.',
|
|
398
|
+
annotations: { destructiveHint: true },
|
|
399
|
+
inputSchema: {
|
|
400
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
401
|
+
rows: z.number().int().min(1).describe('Number of rows (>=1)'),
|
|
402
|
+
cols: z.number().int().min(1).describe('Number of columns (>=1)'),
|
|
403
|
+
index: z.number().int().optional().describe('Character index to insert at (1 = beginning). Omit or use atEnd for end-of-doc.'),
|
|
404
|
+
atEnd: z.boolean().optional().describe('Insert at end-of-doc/tab (mutually exclusive with index)'),
|
|
405
|
+
valuesJson: z.string().optional().describe('Cell values as a JSON 2D string array; dimensions must match rows x cols when supplied'),
|
|
406
|
+
tab: z.string().optional().describe('Target a specific tab by title or ID'),
|
|
407
|
+
account: accountParam,
|
|
408
|
+
},
|
|
409
|
+
}, async ({ docId, rows, cols, index, atEnd, valuesJson, tab, account }) => {
|
|
410
|
+
const args = ['docs', 'insert-table', docId, `--rows=${rows}`, `--cols=${cols}`];
|
|
411
|
+
if (index !== undefined) args.push(`--index=${index}`);
|
|
412
|
+
if (atEnd) args.push('--at-end');
|
|
413
|
+
if (valuesJson !== undefined) args.push(`--values-json=${valuesJson}`);
|
|
414
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
415
|
+
return runOrDiagnose(args, { account });
|
|
416
|
+
});
|
|
345
417
|
}
|
|
@@ -550,6 +550,28 @@ describe('gog_docs_append', () => {
|
|
|
550
550
|
{ account: undefined },
|
|
551
551
|
);
|
|
552
552
|
});
|
|
553
|
+
|
|
554
|
+
// Regression: the tool description must warn about the 3 known upstream
|
|
555
|
+
// markdown converter bugs (openclaw/gogcli#607, #608, #609). If upstream
|
|
556
|
+
// fixes any of these and the warning is removed, this test fails as a
|
|
557
|
+
// prompt to revisit + reopen the README/SKILL/TODO sections too.
|
|
558
|
+
it('description warns about all 3 known upstream markdown limitations', async () => {
|
|
559
|
+
// Local mock to capture the registration config (the shared harness only
|
|
560
|
+
// captures the handler callback).
|
|
561
|
+
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
562
|
+
const server = new McpServer({ name: 'test', version: '0.0.0' });
|
|
563
|
+
const configs = new Map<string, { description?: string }>();
|
|
564
|
+
vi.spyOn(server, 'registerTool').mockImplementation((name, config) => {
|
|
565
|
+
configs.set(name, config as { description?: string });
|
|
566
|
+
return undefined as never;
|
|
567
|
+
});
|
|
568
|
+
const { registerExtraDocsTools } = await import('../../src/tools/docs-extra.js');
|
|
569
|
+
registerExtraDocsTools(server);
|
|
570
|
+
const desc = configs.get('gog_docs_append')?.description ?? '';
|
|
571
|
+
expect(desc).toMatch(/openclaw\/gogcli#607/);
|
|
572
|
+
expect(desc).toMatch(/openclaw\/gogcli#608/);
|
|
573
|
+
expect(desc).toMatch(/openclaw\/gogcli#609/);
|
|
574
|
+
});
|
|
553
575
|
});
|
|
554
576
|
|
|
555
577
|
describe('gog_docs_read', () => {
|
|
@@ -643,4 +665,137 @@ describe('gog_docs_format', () => {
|
|
|
643
665
|
await handlers.get('gog_docs_format')!({ docId: 'd1' });
|
|
644
666
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'format', 'd1'], { account: undefined });
|
|
645
667
|
});
|
|
668
|
+
|
|
669
|
+
// gog 0.18.0
|
|
670
|
+
it('passes --heading-level and --named-style when provided', async () => {
|
|
671
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
672
|
+
const handlers = setupHandlers();
|
|
673
|
+
await handlers.get('gog_docs_format')!({
|
|
674
|
+
docId: 'd1', match: 'Intro', headingLevel: 1, namedStyle: 'HEADING_1',
|
|
675
|
+
});
|
|
676
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
677
|
+
['docs', 'format', 'd1', '--match=Intro', '--heading-level=1', '--named-style=HEADING_1'],
|
|
678
|
+
{ account: undefined },
|
|
679
|
+
);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('passes --heading-level=0 when explicitly 0 (HEADING_0 reserved but argument should reach gog)', async () => {
|
|
683
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
684
|
+
const handlers = setupHandlers();
|
|
685
|
+
await handlers.get('gog_docs_format')!({ docId: 'd1', headingLevel: 0 });
|
|
686
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
687
|
+
['docs', 'format', 'd1', '--heading-level=0'],
|
|
688
|
+
{ account: undefined },
|
|
689
|
+
);
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// --- gog 0.18.0 new tools ---
|
|
694
|
+
|
|
695
|
+
describe('gog_docs_insert_page_break', () => {
|
|
696
|
+
it('inserts at index when --index provided', async () => {
|
|
697
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
698
|
+
const handlers = setupHandlers();
|
|
699
|
+
await handlers.get('gog_docs_insert_page_break')!({ docId: 'd1', index: 42 });
|
|
700
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
701
|
+
['docs', 'insert-page-break', 'd1', '--index=42'],
|
|
702
|
+
{ account: undefined },
|
|
703
|
+
);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('uses --at-end when atEnd is true', async () => {
|
|
707
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
708
|
+
const handlers = setupHandlers();
|
|
709
|
+
await handlers.get('gog_docs_insert_page_break')!({ docId: 'd1', atEnd: true, tab: 'Body' });
|
|
710
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
711
|
+
['docs', 'insert-page-break', 'd1', '--at-end', '--tab=Body'],
|
|
712
|
+
{ account: undefined },
|
|
713
|
+
);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('passes --index=1 when explicitly 1', async () => {
|
|
717
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
718
|
+
const handlers = setupHandlers();
|
|
719
|
+
await handlers.get('gog_docs_insert_page_break')!({ docId: 'd1', index: 1 });
|
|
720
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
721
|
+
['docs', 'insert-page-break', 'd1', '--index=1'],
|
|
722
|
+
{ account: undefined },
|
|
723
|
+
);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
describe('gog_docs_page_layout', () => {
|
|
728
|
+
it('passes --layout when provided', async () => {
|
|
729
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
730
|
+
const handlers = setupHandlers();
|
|
731
|
+
await handlers.get('gog_docs_page_layout')!({ docId: 'd1', layout: 'pages' });
|
|
732
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
733
|
+
['docs', 'page-layout', 'd1', '--layout=pages'],
|
|
734
|
+
{ account: undefined },
|
|
735
|
+
);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('omits --layout when not provided (defaults to pageless on gog side)', async () => {
|
|
739
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
740
|
+
const handlers = setupHandlers();
|
|
741
|
+
await handlers.get('gog_docs_page_layout')!({ docId: 'd1' });
|
|
742
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'page-layout', 'd1'], { account: undefined });
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
describe('gog_docs_insert_table', () => {
|
|
747
|
+
it('passes required --rows and --cols', async () => {
|
|
748
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
749
|
+
const handlers = setupHandlers();
|
|
750
|
+
await handlers.get('gog_docs_insert_table')!({ docId: 'd1', rows: 3, cols: 2 });
|
|
751
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
752
|
+
['docs', 'insert-table', 'd1', '--rows=3', '--cols=2'],
|
|
753
|
+
{ account: undefined },
|
|
754
|
+
);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it('passes --values-json, --index, --tab when provided', async () => {
|
|
758
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
759
|
+
const handlers = setupHandlers();
|
|
760
|
+
const vj = '[["a","b"],["c","d"]]';
|
|
761
|
+
await handlers.get('gog_docs_insert_table')!({
|
|
762
|
+
docId: 'd1', rows: 2, cols: 2, index: 10, valuesJson: vj, tab: 'Body',
|
|
763
|
+
});
|
|
764
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
765
|
+
['docs', 'insert-table', 'd1', '--rows=2', '--cols=2', '--index=10', `--values-json=${vj}`, '--tab=Body'],
|
|
766
|
+
{ account: undefined },
|
|
767
|
+
);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('uses --at-end when atEnd is true', async () => {
|
|
771
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
772
|
+
const handlers = setupHandlers();
|
|
773
|
+
await handlers.get('gog_docs_insert_table')!({ docId: 'd1', rows: 1, cols: 1, atEnd: true });
|
|
774
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
775
|
+
['docs', 'insert-table', 'd1', '--rows=1', '--cols=1', '--at-end'],
|
|
776
|
+
{ account: undefined },
|
|
777
|
+
);
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
describe('gog_docs_comments_reopen', () => {
|
|
782
|
+
it('calls runOrDiagnose with docId and commentId', async () => {
|
|
783
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
784
|
+
const handlers = setupHandlers();
|
|
785
|
+
await handlers.get('gog_docs_comments_reopen')!({ docId: 'd1', commentId: 'c1' });
|
|
786
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
787
|
+
['docs', 'comments', 'reopen', 'd1', 'c1'],
|
|
788
|
+
{ account: undefined },
|
|
789
|
+
);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('forwards account', async () => {
|
|
793
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
794
|
+
const handlers = setupHandlers();
|
|
795
|
+
await handlers.get('gog_docs_comments_reopen')!({ docId: 'd1', commentId: 'c1', account: 'a@b.com' });
|
|
796
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
797
|
+
['docs', 'comments', 'reopen', 'd1', 'c1'],
|
|
798
|
+
{ account: 'a@b.com' },
|
|
799
|
+
);
|
|
800
|
+
});
|
|
646
801
|
});
|