npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@simon-bouchard/google-docs-mcp

v0.6.0

Published

A local, stdio-based MCP server for reading and anchor-based editing of existing Google Docs.

Readme

google-docs-mcp

Status: early and actively developed. The core functionality below is tested and live-verified against real Google Docs, but expect rough edges. Server bugs are reported automatically (see Automatic issue reports); for anything else, use /google-docs-mcp:feedback or just ask.

A local, stdio-based MCP server that lets Claude Code (or any MCP client) read and edit existing Google Docs. It authenticates as a Google Cloud service account, or, once the Koios OAuth client exists, as you after a one-time browser sign-in, and exposes read_document, read_comments and edit_document, plus report_issue, which Claude uses to file the server's own bugs and limitations.

Edits are anchor-based: you address content by search text or heading text, never by raw character offsets. All the operations in one edit_document call are sent as a single atomic batchUpdate, guarded by the document's revision id, so an edit against a stale read is refused rather than silently applied to changed content. Every anchor-resolving operation also echoes back a short snippet of the text it actually touched, so a caller can confirm it hit the right spot without a follow-up read.

Setup

  1. Create a Google Cloud project. Any Google account works; it does not need to be attached to a Workspace organisation.

  2. Enable the Google Docs API for that project (APIs & Services -> Library -> "Google Docs API" -> Enable). This step is easy to miss: having a project and a service account does not mean the API is enabled. If you skip it, every call fails with a SERVICE_DISABLED 403, which reads like "the doc isn't shared with me" but is not.

  3. Optional: enable the Google Drive API the same way, if you want Claude to read comments. It is only used for read_comments and for reading which checklist items are ticked, with a read-only scope that cannot change files or sharing. Without it, read_comments returns drive_api_disabled, checklist items come back without checked, and everything else works.

  4. Enable the Google Sheets API the same way. It is only used to file issue reports into the shared feedback spreadsheet; without it, automatic issue reports and /google-docs-mcp:feedback can't be filed, and everything else works.

  5. Create a service account (IAM & Admin -> Service Accounts). It needs no project roles -- its access comes entirely from document sharing.

  6. Download a JSON key for it (Keys -> Add Key -> Create new key -> JSON) and install it:

    mkdir -p ~/.config/google-docs-mcp
    mv ~/Downloads/<your-key>.json ~/.config/google-docs-mcp/service-account.json
    chmod 600 ~/.config/google-docs-mcp/service-account.json
  7. Share each Doc you want to read or edit with the service account's email (shown on the service account's details page, of the form <name>@<project>.iam.gserviceaccount.com) as Editor. A service account can see nothing it has not been explicitly shared.

  8. Send the service account's email to the maintainer so it can be added as an editor of the shared feedback spreadsheet (also used by google-sheets-mcp). Without it, automatic issue reports from your sessions fail silently.

Signing in with your Google account (not yet available)

A second auth mode is built in: sign in once in the browser and the server acts as you, seeing exactly the files you can see, with no key file or sharing step. It waits on a Koios Workspace admin creating the Internal OAuth client; until then, use the service account above. Both modes can stay installed side by side.

  1. Get the OAuth client (a Desktop app client in a Koios-owned Cloud project) and save its JSON at ~/.config/google-workspace-mcp/oauth-client.json, or set GOOGLE_WORKSPACE_MCP_CLIENT_ID (and GOOGLE_WORKSPACE_MCP_CLIENT_SECRET). Once the Koios client exists it will ship with the package and this step goes away.

  2. Sign in:

    npx -y @simon-bouchard/google-docs-mcp login

    The browser opens Google's consent page; the token is saved at ~/.config/google-workspace-mcp/token.json (mode 600). The sign-in is shared with google-sheets-mcp, so signing in through either server covers both.

  3. Restart Claude Code so the server picks up the sign-in.

Which mode is used: a saved sign-in wins over a service account key. Set GOOGLE_DOCS_MCP_AUTH=service_account (or oauth) to force one, for example to go back to the service account without signing out. npx -y @simon-bouchard/google-docs-mcp logout deletes the saved token; to also revoke access, remove the app at https://myaccount.google.com/connections. When signed in, issue reports are written as you, so the feedback spreadsheet must be editable by you rather than by a service account.

GOOGLE_WORKSPACE_MCP_CONFIG_DIR moves the shared directory.

Install

npm install -g @simon-bouchard/google-docs-mcp

Or run it without installing, via npx (used directly in the registration command below).

Installing as a Claude Code plugin

Instead of registering the MCP server by hand, you can add this repo as a Claude Code plugin source and install it as a plugin -- this also gives you the /google-docs-mcp:feedback command (see Reporting feedback yourself):

claude plugin marketplace add https://bitbucket.org/koiosemployee/google-docs-mcp.git
claude plugin install google-docs-mcp

This still requires the service account setup (or a sign-in) above -- the plugin only changes how the server is registered and adds the feedback command, not the auth model.

To save memory, also install the server globally (npm install -g @simon-bouchard/google-docs-mcp). The plugin then runs it directly instead of through npx, which otherwise keeps a ~75 MB process alive for the whole session. Without a global install it falls back to npx, which always fetches the latest version; with one, update it by re-running the same npm install -g.

Automatic issue reports

When the server hits one of its own bugs (an unexpected internal_error) or content it can't handle yet (unsupported_content), the error tells Claude to file it in the background with report_issue, and the server attaches what Claude can't see: its version, the failed call's operations, the stack trace, and for bugs a structure summary of the document (counts of paragraphs, headings, lists, tables, merged tables, embedded element kinds, header/footer; no text). Claude also files, following the server's instructions, when an edit succeeded but did something other than what was asked (its echoed context or a read-back shows it, or you point it out; Claude reports even when unsure whose fault it is), or when you ask for a Google Docs task that none of this server's tools can do (a limitation). Those reports carry the server's last three calls, reduced the same way as below, and the structure summary of the last document they touched.

Never reported: errors that are working as designed (concurrency_conflict is a safety guard; anchor errors like ambiguous_match and request mismatches like invalid_input, such as a new table row with the wrong number of cells, are fixable input), setup problems (auth, sharing, rate limits, the Drive API not being enabled), network problems and Google outages, and requests meant for other tools or servers.

Reports go to the maintainers' feedback spreadsheet, shared with google-sheets-mcp (Bugs and Limitations tabs, Source column auto, Server column google-docs-mcp), at most once per problem per session. Claude mentions each one in a single line. What gets written is kept to what's needed to reproduce the problem:

  • The document id is removed; anchor and inserted text become their length ([text, 12 chars]), and table content its shape ([3x2 cells]).
  • Claude describes document content instead of quoting it.
  • Credentials are scrubbed from every field before anything is written (tokens, keys, passwords, Authorization headers, OAuth client secrets, private keys, long random strings).

To turn it off, set GOOGLE_DOCS_MCP_AUTO_REPORT=off. To let reports go through without a permission prompt, allow report_issue (see Registering).

Reporting feedback yourself

If you installed this as a plugin, /google-docs-mcp:feedback [bug|limitation] [notes] drafts a report from the conversation, shows it to you, and files it through report_issue after you confirm, even when automatic reports are turned off. Anything you write after the kind word is kept verbatim as Reporter notes, and the row's Source is manual with notes (or manual without notes).

Reports from both servers are triaged in the same spreadsheet: each row stays for good and gets a Status (in progress, fixed, won't fix, duplicate; empty means new) and a free-text Resolution. The google-sheets-mcp README describes the columns and the maintainer setup. Here, the destination is DEFAULT_FEEDBACK_SHEET_ID in src/reporting.ts, which users can override with GOOGLE_DOCS_MCP_FEEDBACK_SHEET_ID (the older GOOGLE_DOCS_MCP_FEEDBACK_DOC_ID and GOOGLE_DOCS_MCP_LIMITATIONS_DOC_ID are no longer read). The server needs the spreadsheets scope only to append those rows. That also means the Google Sheets API must be enabled in your Cloud project (setup step 4).

Registering with Claude Code

Register it as a user-scoped MCP server (mcpServers is not a valid settings.json field -- this is the actual mechanism):

claude mcp add --scope user google-docs -- npx -y @simon-bouchard/google-docs-mcp

(If you installed it globally instead, -- google-docs-mcp works too, without npx.)

Tool permissions go in ~/.claude/settings.json. Tool names are mcp__<server name>__<tool>, so with the command above (server name google-docs):

  • To be prompted before every edit, add mcp__google-docs__edit_document to permissions.ask.
  • For automatic issue reports to really happen in the background, add mcp__google-docs__report_issue to permissions.allow; otherwise Claude Code stops to ask for permission the first time a report is filed. Run /mcp to see the exact server name if you installed it as a plugin.
{
  "permissions": {
    "allow": ["mcp__google-docs__report_issue"],
    "ask": ["mcp__google-docs__edit_document"]
  }
}

What it can do

read_document

Returns the document's plain text, its heading outline, per-paragraph formatting (heading level, list membership, spacing, border, background shading), each run's character style, and the revision id needed for edit_document. Long runs of embedded non-text content (many images in a row, for example) are compressed to a short [N embedded objects] marker in the returned text, rather than repeating a placeholder character hundreds of times on every call.

Checklist items report list: "checklist" and checked (whether the box is ticked). The Docs API doesn't expose ticked state, so the server reads it from Drive's Markdown export of the document, only when the document has checklist items. If the export fails or doesn't line up with the document, checked is left out rather than guessed.

header and footer report the document's default header/footer, in the same shape (text plus per-paragraph formatting) as the body -- null for either one the document doesn't have.

embeddedElements lists where non-text content (images, page breaks, footnotes, equations, horizontal rules, column breaks, section breaks, table of contents, and a few rarer link/auto-text kinds) sits -- each entry gives its type, a short text snippet for context, and which segment (body/header/footer) it's in. Check this before constructing a find/replace: the placeholder character marking an embedded element's own spot in text is a single easy-to-miss character, and a find spanning across one is refused outright rather than silently deleting it, so knowing where these are up front avoids that round-trip.

format: "markdown" returns the tab as Markdown instead, from Drive's Markdown export: often a third of the size or less, since it leaves out per-paragraph formatting, embeddedElements and the header and footer. It still returns headings and revision_id, so edit_document can follow directly, but find matches the document's plain text, so it has to be written without Markdown syntax (**, backslash escapes, list markers, table pipes). Images show as [image] instead of their embedded data, and a tab keeps its own footnotes. The export holds every tab, and each tab's part is found by tab order and checked against that tab's text; if it doesn't line up, the whole export comes back with markdown_covers_all_tabs: true. Like checklist ticked state, it needs the Drive API.

section reads only the part of the tab under one heading (its exact text, as headings lists it, matching a single heading): from that heading to the next heading of the same or a higher level, subheadings included, in either format and without the header and footer. revision_id is still the whole document's, so an edit can follow a section read exactly as it follows a full one. In Markdown, the section's lines are checked against its text; if they don't line up, its plain text comes back instead, with section_as_plain_text: true. On long documents this keeps a read to the part Claude needs.

Document tabs

Every tool works on one tab at a time. read_document and edit_document take an optional tab, a tab's title or id. Without it they use the tab a pasted link points at (.../edit?tab=t.abc), or else the first tab. A document with several tabs also reports tab, the tab read, and tabs, every tab with its id (a child tab carries its nestingLevel), so Claude can find the tab it needs. An unknown tab, or a title several tabs share, is refused with the list of tabs. On a document with several tabs, edit_document requires tab (unless the link names one), so an edit never lands in the first tab by default: pass the same tab you read. Every operation in the call goes to that tab, its header, footer and footnotes included, and the response echoes it. base_revision_id covers the whole document, so an edit in another tab since the read also counts as a conflict.

read_comments

Lists the document's comments, oldest first: author, time, resolved status, text and replies (including whether a reply resolved or reopened the comment). quoted_text is the text the comment is attached to, and quoted_text_matches says how many times it appears in the document now, so Claude knows whether it can use it directly as edit_document's find. A count of 0 doesn't always mean the text changed: a quote spanning a table, image, line break or footnote can be written differently in the document text. In a document with several tabs, the count is the total over all tabs, quoted_text_tabs gives each tab holding the text with its own count (occurrence counts within one tab), and unreadable_tabs lists any tab that couldn't be searched. Resolved comments are hidden unless include_resolved is true, deleted ones are never shown, and at most 500 comments are returned (truncated says when there were more). Read only: this server cannot reply to, resolve or create comments.

edit_document

Applies a list of operations atomically in one batchUpdate, guarded by base_revision_id. Every operation that resolves a target locates it either by find (matched against the document's plain text -- must match exactly once, or pass occurrence to pick the Nth match when it legitimately repeats) or by heading (the exact text of a heading, which only needs to be unique among headings, not the whole document). A find that matches nothing is retried with whitespace normalized (catching a non-breaking space where you typed a regular one, or similar) before failing, so the error can suggest the real near-match text instead of just "not found."

replace_text, format_text, and set_paragraph_style also accept a segment: "header" | "footer" parameter, which anchors find/occurrence against the document's default header/footer instead of its body (not valid alongside heading -- heading-anchoring is body-only). When segment is set, find and heading may both be omitted to target the segment's entire content directly (whole-segment addressing). replace_text and set_paragraph_style both work this way even on a freshly created, still-empty header/footer (there's nothing yet to find in it) -- use replace_text to add its first text. format_text needs the segment to already have text before it can style it this way.

Operation types:

  • replace_text -- replace found text (or a heading's own text) with new text. An empty replace deletes the matched span outright.

  • format_text -- apply character-level style (bold, italic, underline, strikethrough, font family/size, color) to found text, or make it a hyperlink with link (a URL; null removes the link). link: "#<heading text>" links to a heading of the same tab instead, named by its exact text or a GitHub-style slug (#api-design); it must match one heading. read_document reports a linked run's link, a link to a heading of the tab as "#<heading text>".

  • set_paragraph_style -- heading level, list membership (bulleted/numbered/checklist/none), line spacing, space before/after, border (color/width/padding, optionally narrowed to a sides subset for an accent-border look), and background shading.

  • format_table_cells -- background color and border for a table cell (found by matching text inside it), a rectangular block of cells via rowSpan/columnSpan, or the whole table via wholeTable. Works in tables with merged cells too.

  • insert_table -- create a new table (empty, or pre-filled via a content 2D array), appended at the document's end or placed before/after a heading or found paragraph.

  • insert_table_row / insert_table_column -- grow an existing table, optionally filled with initial content, anchored to a cell found in it.

  • delete_table_row / delete_table_column -- shrink an existing table.

  • append_paragraph -- add a new paragraph at the end of the document.

  • insert_after_heading / insert_before_heading -- add a new paragraph immediately after or before a heading.

  • create_header / create_footer -- add a default header/footer to the document. Refuses if the document already has one (edit that instead, via segment). Structure-only: the new segment starts out empty. Fill it with a following replace_text operation that sets segment and omits both find and heading (whole-segment addressing), which targets the segment's entire content directly and works even while it's still empty. format_text and set_paragraph_style accept the same segment whole-segment addressing, but format_text needs the segment to already have text first.

  • insert_page_break -- start a new page before or after a heading or found paragraph, or end the document with a break. Same placement as insert_table. Not possible inside a table or a header/footer.

  • add_footnote -- add a footnote with the given text right after found text. It must be the only operation in its edit_document call: Google only returns the new footnote's id after creating it, so its text is a second write. If that write fails, the empty footnote is removed again. read_document lists footnotes in embeddedElements with their text.

  • delete_paragraph -- delete a whole paragraph: the one a heading or found text is in, or one offset paragraphs away from it. When the anchor is text inside a table, the offset counts from the table (1 is the paragraph right after it), which reaches empty or image-only paragraphs that find can't match. expectedText ("" for an empty paragraph) refuses the deletion if the paragraph's text differs. The last paragraph and one right before a table are merged into the paragraph before them, which keeps its style. Not inside table cells.

  • insert_markdown -- add formatted content written as Markdown (GitHub flavor), placed like insert_table: before or after a heading or found paragraph, or at the end of the document. Headings (levels 1-6), paragraphs, bulleted, numbered and checklist items (nested by indentation), bold, italic, strikethrough, inline code (Roboto Mono), links, fenced code blocks (monospace lines on a light gray background), block quotes (a left border), tables (header row in bold) and --- rules (a bottom border, since the API can't insert a real horizontal rule). A nested list takes its top-level list's style, since Docs gives one list a single bullet style; checklist items start unticked; absolute http, https and mailto links are kept, and [text](#anchor) links to a heading already in the tab (its text URL-encoded, or a slug like #api-design); any other link, or an anchor naming no single heading (such as one added in the same call), stays plain text; an image becomes its alt text linked to the image; HTML tags and comments are dropped. Content placed after a paragraph that is followed by a table gets a paragraph of its own between the two. Appending fills an empty last paragraph rather than leaving a blank line, and no blank line is left before a table.

  • merge_table_cells -- merge the rowSpan x columnSpan block of cells whose top-left cell holds the found text; their text moves into that cell. A block that cuts through an existing merged cell is refused (include the whole merged cell, or unmerge it first).

  • unmerge_table_cells -- split the merged cell holding the found text back into its cells; the text stays in the first one, so a merged cell is found by that text. read_document doesn't show which cells are merged. Rows and columns can't be inserted or deleted in a table with merged cells, so unmerge first; a merge and a row/column change on the same table, or two overlapping merges, go in separate calls.

Every operation that pins ambient style (bold leaking from an adjacent run, a heading's style duplicating onto newly split text) explicitly resets it, so inserted or edited text always ends up exactly as styled as requested, never inheriting whatever happened to be nearby.

report_issue

Files a bug or a limitation into the shared feedback spreadsheet (see Automatic issue reports). Claude calls it on its own when a failed call's error says to, or when it hits a limitation; you don't need to call it yourself.

Non-goals

  • No creating new Google Docs (only editing existing ones).
  • No Drive file management (move, rename, delete, share).
  • No Sheets or Slides (for Sheets, see google-sheets-mcp).
  • No writing comments and no suggestions (tracked-changes) mode. Comments can be read (see read_comments), but creating one reliably anchored to a specific text range isn't supported by the public API, and replying or resolving would need a Drive scope that can also change sharing.
  • No ticking or unticking checklist items: Google's API has no way to change a checkbox, so new checklist items always start unticked. Ticked state can be read (see read_document).
  • No image insertion, replacement, or resizing -- a document merely containing an image is fully supported (it's represented as a placeholder character rather than blocking the rest of the document), just not creating or editing one.
  • No independent per-side border colors -- sides narrows a single border style to a subset of sides, not a different color per side.

Roadmap

  • Ship the Koios OAuth client with the package. The sign-in mode is built; it needs a Koios Workspace admin to create an Internal OAuth client in a Koios-owned Cloud project. Once it exists, it goes into BUNDLED_OAUTH_CLIENT (src/oauth.ts, here and in google-sheets-mcp) so signing in needs no client file. The service-account mode stays as a fallback.

Developing

To work on the server itself rather than just install it:

git clone [email protected]:koiosemployee/google-docs-mcp.git
cd google-docs-mcp
npm install
npm run build
npm test

npm run dev runs it directly from TypeScript source (via tsx) without a build step, and npm run smoke-test (with DOCS_MCP_TEST_DOC_ID set to a real scratch Google Doc's id or URL) exercises read_document/read_comments/edit_document against a live document -- useful for confirming a change works against the real API, not just the unit tests.