MCP server for Google Docs with verified writes β every mutation returns before/after proof.
Copy the AI prompt to install this server into Claude Code, Cursor, or another agent β or use 1-click editor setup below.
We haven't yet run this listing's install command through our automated sandbox check. This isn't a red flag β we're steadily working through the catalog.
π‘ Paste the JSON block into your client's configuration file under mcpServers, then restart the application.
An MCP server for Google Docs whose writes carry proof. Every mutating tool re-reads the affected content from the document after it writes and returns evidence of what actually changed: before/after excerpts, the match count, and the document revision before and after. A tool never reports success for an edit that did not land.
Status: all 20 tools are implemented, covered by an offline unit suite, and exercised against the real Google Docs and Drive APIs by the live test suite. The original fourteen passed the formal live acceptance gate; the gate is rerun before each release. Install with
uvx verified-googledocs-mcp. See Status.
Driving Google Docs from an agent through a general Workspace MCP server tends to fail in quiet, expensive ways:
findAndReplace meant for one tab silently edits every tab in the document.Each of these has a procedural workaround: tell the agent to scope to a tab, retry with normalized quotes, re-read after every write, never trust a resolve result. Those instructions work until someone forgets one. This server moves the discipline into the protocol, where it is deterministic.
Every mutating tool runs the same pipeline: read the tab, locate the target, apply the edit under a revision precondition, read the tab again, and return evidence built from the second read. The return value is a claim about the document's state after the call, backed by a server-side re-read, not an echo of the API response.
When something is wrong, the tool fails loud and diagnosed, with a typed error the agent can act on in one round trip rather than guessing:
tab_id. There is no whole-document replace, so a one-tab edit can never leak into a cover letter or an appendix tab.expected_matches defaults to 1. If the real count differs, the tool makes no edit and returns every match location.writeControl.requiredRevisionId from the pre-read, so a document that changed underneath the operation is rejected by the API rather than edited blind. Section ranges from find_sections are stamped with the revision they were computed at and refuse to apply once stale.audit_logged: false).The guarantee is not one universal payload β it is a per-family invariant. Each
family re-reads the document after the write and proves the property that family
is responsible for. Every mutating tool also carries revision_before,
revision_after, and audit_logged.
| Family | Tools | Proves |
|---|---|---|
| Text edit | replace_text | match_count equals expected_matches; rung names the normalization pass that matched; before/after are Β±200-char excerpts of the edited span, the after re-read post-write |
| Style edit | format_text | runs_before/runs_after β the actual textRun style flags overlapping each matched span, not a text diff; content_mutated: false proven by compiled_request_kinds containing only updateTextStyle; style_mutated false on an idempotent re-run (no write is issued) |
| Markdown range | replace_range_markdown, replace_tab_markdown, append_markdown | structural_match (the written markdown round-trips β including list nesting depth, ordered-vs-unordered, and link targets), input_blocks vs post_blocks counts, and a structural_diff list naming any mismatch; every response also carries write_status (not_written / written_unverified / written_verified) and retry_safe |
| Structural | insert_image | inline_object_confirmed β a post-write scan found the inline object at the anchor paragraph |
| Comment state | add_anchored_comment, reply_to_comment, resolve_comment | the re-queried resolved flag, reply_count, content, quoted_text, and author β a resolve that didn't land returns COMMENT_STILL_OPEN, never success |
| Table | replace_table_row, insert_table | replace_table_row: row_before/row_after re-read with cells_match; insert_table: table_confirmed plus the confirmed dimensions and first_row |
The read and sync tools (read_document, list_tabs, find_sections,
list_open_items, get_comment_thread, diff_tab_vs_file, list_tables,
get_table) make no changes and carry no applied/evidence payload.
export_pdf is also a read/export tool in this sense β it returns file facts
(bytes_written, sha256, page_count) rather than an applied key, since
nothing in the document changes.
Eight mutating tools (replace_text, format_text, replace_range_markdown,
replace_tab_markdown, append_markdown, insert_image, replace_table_row,
insert_table) accept dry_run=true. No API write is issued; the response
carries applied: false, an empty revision_after (no write, so no new
revision), audit_logged: false, and β for replace_text β a predicted
after excerpt computed by splicing the replacement into the pre-read, or β
for format_text β a predicted runs_after computed by overlaying the
requested style onto the pre-read runs. format_text additionally never
issues a write at all when the matched text already carries every requested
style value, dry run or not β that's the tool's normal idempotent no-op path,
not specific to dry_run. For replace_table_row and insert_table,
dry_run is authoritative for index validity: the same assembled request
list is index-simulated whether dry_run is true or false, so a passing dry
run always means the real write will pass too. Use it to confirm a locate
resolves to the right span before committing the edit.
For the three markdown tools, dry_run is also authoritative for
structure prediction (issue #65): before any batchUpdate, the compiled
requests' own predicted block structure β nesting, ordered-vs-unordered,
headings, tables β is compared against the parsed input markdown, identically
in dry_run and the real write. A mismatch refuses with
STRUCTURE_PREDICTION_FAILED before the document is touched, rather than
mutating it and only then discovering post-write verification would have
failed. Every response from these three tools β dry run, refused, or
written β carries write_status, one of not_written (dry run, or a
pre-flight refusal: INDEX_SIMULATION_FAILED, STRUCTURE_PREDICTION_FAILED,
or the structural-loss guardrail), written_unverified (a batchUpdate was
sent and accepted, but the post-write re-read did not confirm it matched β
VERIFICATION_FAILED), or written_verified (confirmed) β plus a
retry_safe boolean (false only for written_unverified, since a caller
that retries a call whose mutation status is unconfirmed risks a second
mutation on top of an unconfirmed first one; there is no automatic rollback β
Docs has no revision-restore endpoint, and a compensating markdown rewrite
would itself be lossy, so needs_manual_restore: true still means restoring
from Docs version history). write_status/retry_safe are additive: the
pre-existing applied field keeps its documented meaning unchanged.
No reviews yet β be the first to share how this listing worked for you.
Showcase your server listing on GitHub or your project documentation. Embed this dynamic SVG badge to highlight official listing status and live engagement.
[](https://allmcps.com/mcp/verified-googledocs-mcp)<a href="https://allmcps.com/mcp/verified-googledocs-mcp"><img src="https://allmcps.com/api/badge/verified-googledocs-mcp?style=directory" alt="Verified Googledocs MCP on AllMCPs" /></a>