google-docs-mcp
by YerayRodri
README.md
# google-docs-mcp
MCP server to read, create and edit Google Docs. Built for report/audit
workflows: clone a template, fill sections by heading, find/replace
placeholders, round-trip content as Markdown, and make surgical edits
(formatting, tables, headers/footers) without regenerating the whole document.
## Tools (23)
### Reading
| Tool | What it does |
|---|---|
| `read_document` | Read the full document — as Markdown (default, keeps structure) or plain text |
| `get_document_structure` | Index of H1–H6 headings with their position |
| `get_headings` | List all headings with level and text, without loading full content |
### Basic text editing
| Tool | What it does |
|---|---|
| `insert_text_after_heading` | Insert text right after a section (partial, case-insensitive heading match) |
| `append_text` | Append text at the end of the document |
| `find_replace` | Find and replace text (handy for `{{PLACEHOLDER}}`-style templates) |
### Documents
| Tool | What it does |
|---|---|
| `create_document` | Create a new empty document |
| `copy_document` | Clone an existing document (e.g. from a template) |
### Native Markdown round-trip
| Tool | What it does |
|---|---|
| `create_document_from_markdown` | Create a Google Doc from Markdown with native formatting — headings land in the document outline, and bold, italics, links, tables, lists and quotes are preserved |
| `replace_document_from_markdown` | Replace a whole existing Doc's content from Markdown, keeping the same document ID/URL so shared links and permissions don't break |
### Images and page layout
| Tool | What it does |
|---|---|
| `insert_image` | Insert an image from a public URL (e.g. a chart) |
| `insert_page_break` | Insert a page break — useful for reports meant to print or export to PDF |
| `set_header` | Set (or replace) the document header text, repeated at the top of every page |
| `set_footer` | Set (or replace) the document footer text |
### Named ranges (reusable templates)
| Tool | What it does |
|---|---|
| `add_named_range` | Name a range of text so it can be referenced later without depending on indexes that shift as the document is edited |
| `replace_named_range_content` | Replace a named range's content with new text, without touching the rest of the document — the core piece for monthly-report-style templates |
| `delete_named_range` | Remove a range's name (doesn't delete the text, just the reference) |
### Surgical formatting
| Tool | What it does |
|---|---|
| `format_text` | Apply formatting (bold, italic, underline, color, font size) to a specific text range, without touching the rest of the document |
| `align_paragraph` | Change the alignment of one or more paragraphs (left, center, right, justified) |
### Tables
| Tool | What it does |
|---|---|
| `insert_table` | Insert a new table into the document without touching the rest of the content |
| `insert_table_row` | Insert a row into an existing table |
| `delete_table_row` | Delete a row from an existing table |
| `merge_table_cells` | Merge table cells — e.g. for a title spanning several columns |
## ⚠️ Note — character indexes go stale after any edit
`format_text`, `align_paragraph`, `add_named_range` and the table tools all work with character
indexes (`start_index`/`end_index`/`table_start_index`...). **Any prior edit to the document —
even a one-character change — shifts every index that comes after it.**
This is how Google Docs' index model works, not a bug in these tools: verified in production by
capturing an index, making an unrelated edit in between (one that changed the length of the
document), and then reusing the stale index — the formatting landed on the wrong paragraph.
Re-reading the same index right before the call, with no edit in between, hit the correct
paragraph both times.
**Rule of thumb: fetch a fresh index (`get_document_structure` or a direct read) immediately
before calling any of these tools if anything else may have touched the document in between.
Never reuse an index from an earlier step.**
## Typical workflow
```
1. copy_document(template_id, "Client Report — August 2026")
2. find_replace("{{CLIENT}}", "Acme Inc.")
3. find_replace("{{DATE}}", "August 2026")
4. get_document_structure() → see available headings
5. insert_text_after_heading("TRAFFIC OVERVIEW", report_data)
```
Or, for a document authored in Markdown:
```
1. create_document_from_markdown(markdown_text, title="Monthly Report")
... later, to update it in place ...
2. read_document(doc_id) → Markdown with full structure
... edit the Markdown ...
3. replace_document_from_markdown(doc_id, md) → same ID, same URL
```
## Setup
1. Create a Google Cloud project (or reuse one) and enable the
**Google Docs API** and **Google Drive API** (needed for `copy_document`
and the Markdown import/export tools).
2. Create an OAuth 2.0 Client ID of type "Desktop app" and download it as
`client_secret.json`.
3. If the app is in "Testing" mode, add your Google account as a test user.
4. Install dependencies:
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
5. Run the OAuth flow once:
```bash
CLIENT_SECRET_PATH=~/.config/google-docs-mcp/client_secret.json \
python3 setup_auth.py
```
This opens a browser — log in and grant access.
## MCP client configuration
```json
{
"mcpServers": {
"google-docs": {
"command": "/path/to/.venv/bin/python3",
"args": ["/path/to/google-docs-mcp/server.py"],
"env": {
"GOOGLE_WORKSPACE_TOKEN_PATH": "~/.config/google-docs-mcp/token.json"
}
}
}
}
```
| Env var | Default | Purpose |
|---|---|---|
| `GOOGLE_WORKSPACE_TOKEN_PATH` | `~/.config/google-workspace-mcp/token.json` | Path to the OAuth token |
## Safety
- Every tool carries MCP Tool Annotations from the spec (`readOnlyHint`, `destructiveHint`,
`idempotentHint`, `openWorldHint`).
- Execution errors propagate as real MCP protocol errors (`isError=true`) — never as a JSON
payload that looks like a success.
- `replace_document_from_markdown` overwrites the entire document body (recoverable from Google
Docs' own version history) — review the target document ID before calling it.
## Notes
- `insert_text_after_heading` does a case-insensitive substring match, so
"traffic" matches "TRAFFIC OVERVIEW". If the heading isn't found, the
response includes `available_headings` so you can retry with the correct one.
- `copy_document` uses the Drive API (`files().copy()`).
- Markdown round-trip preserves headings, bold/italic, links, tables, numbered and nested lists,
quotes, inline code and task checkboxes (`- [ ]` / `- [x]`). Fenced code blocks are the one
exception: the content survives but the ``` markers don't round-trip.
- `merge_table_cells`: the absorbed cell isn't removed from the table's JSON, it's left empty —
the merge itself shows up in the first cell's `columnSpan`/`rowSpan`, not in the cell count.
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues