mcp server
blogger-mcp
MCP server for Blogger: draft, edit, publish and schedule posts, manage pages and moderate comments from Claude or any MCP client. Includes an Agent Skill.
Set up
Runs on your computer. Pick your agent and operating system.
Before you start
- uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
{
"mcpServers": {
"blogger-mcp": {
"command": "uvx",
"args": [
"blogger-mcp"
]
}
}
}same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12
Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs
README
synced Oct 21, 2026blogger-mcp
An MCP server for Blogger, built on the Blogger API v3.
It gives an MCP client — Claude Code, Claude Desktop, or anything else that speaks the protocol — a set of tools for working with a Blogger blog: list and search posts, create drafts, edit them, publish or schedule them, take them back down, manage static pages, and moderate comments. The intended use is turning documents you already have into blog posts without leaving the tool you wrote them in.
It is Python, it runs locally over stdio, and it authorizes as you through a normal Google OAuth consent flow. There is no hosted component and nothing leaves your machine except calls to Google's API.
Status and maintenance
Provided as-is, with no promise of support. It is published because the code works and because several of the Blogger API's undocumented behaviours (listed below) took real effort to find and are worth writing down.
Concretely, that means:
- No commitment to respond to issues or pull requests, and no release schedule.
- No commitment to keep up with changes to the Blogger API. It is a stable, long-lived API, but if Google changes something this may quietly break.
- Bug reports are welcome and may sit unanswered. Forking is entirely fine — the MIT license is there for exactly that.
If you need something dependable for production, read the code first and plan to maintain your own copy.
What it can and cannot do
The Blogger API v3 is the ceiling here, and it has real gaps. Worth knowing before you build a workflow on top of this.
Supported
- Posts: list / get / search / create / update / publish / schedule / revert to draft / delete
- Pages (static pages like About): list / get / create / update / delete
- Comments: list and moderate (approve, mark spam, remove content, delete)
- Blogs: read only — list the account's blogs, look one up by id or URL
- Pageviews: coarse 7-day / 30-day / all-time counts
Not supported — by the API itself, not by this server
- No image upload. There is no media endpoint. Post bodies are HTML, and
every
<img>must already point at a publicly reachable absolute URL. Host images elsewhere first (Drive, GitHub, an image host) and paste in the URL. - No creating, renaming or deleting a blog. The
blogsresource is read-only. - No theme, template, layout, widget or blog-settings editing.
- No label management. Labels are just strings on a post; renaming a label across a blog means patching every post that carries it.
- No posting or replying to comments. Moderation only.
- No Markdown. Content is raw HTML (this server can also wrap plain text).
- No per-post metadata of your own. The
customMetaDatafield exists in the API schema and is documented as "The JSON meta-data for the Post", but Blogger discards writes to it — the value is already null in the insert response. There is no place to record which local file produced a post.
Undocumented API behaviour worth knowing
These were found by running against a live blog. None of them appear in the reference documentation, and three fail silently:
fetchBody=Falseon a write destroys the post body. It reads as a response-shaping option, and is one onposts.get. Onposts.insertandposts.patch, Blogger applies it to the write and blanks the content. No error is returned. This server never sends it on a write.customMetaDatais a no-op. Writes are discarded silently; the value comes back null in the insert response itself.- Whether a delete goes to the trash depends on the post's state, not
just on
useTrash. Deleting a published or scheduled post withuseTrash=truemoves it toSOFT_TRASHED, whereposts.revertrestores it as a draft with its content. Deleting a draft removes it for good even withuseTrash=true. Deleting something already in the trash removes it for good — sodeleteis not idempotent, and retrying one is not safe. Pages behave the same way. blogs.listByUserrejectsview=ADMINwith HTTP 400, even though the API's own discovery document listsADMINas a valid value for that parameter. Every other resource accepts it.posts.searchignores whose blog it is. It is the one read that takes noviewparameter, so it will search any public blog: a mistypedblog_idthat happens to belong to someone else returns their posts, with no error. Every other read, givenview=ADMIN, rejects that with HTTP 403. This server checks admin access before searching for that reason.- Enum values are UPPERCASE.
orderBy=PUBLISHED,sortOption=DESCENDING, commentstatus=PENDING. The HTML reference page documents several of them in lowercase, which the client library rejects outright. - Post creation is rate limited fairly aggressively. A burst of a dozen
creates returns HTTP 429
rateLimitExceeded; it recovers within a couple of minutes. There is no automatic retry in this server.
The git history contains the isolation tests for each of these.
Install
Requires Python 3.11 or newer. 3.10 is not supported: it reached end of life in October 2026, and Google's client libraries have stopped releasing for it.
uv venv
uv pip install -e .
Setup
1. Google Cloud project (once, ~5 minutes)
- Create a project at https://console.cloud.google.com/.
- Enable the Blogger API v3 for it (APIs & Services → Library → "Blogger API v3" → Enable).
- Configure the OAuth consent screen:
- User type External
- Add the scope
https://www.googleapis.com/auth/blogger - Add your own Google account as a test user
- APIs & Services → Credentials → Create credentials → OAuth client ID → application type Desktop app. Download the JSON.
- Save it as
~/.config/blogger-mcp/client_secret.json(or point theBLOGGER_MCP_CLIENT_SECRETSenvironment variable at wherever you put it).
Expect to re-authorize about once a week. While an external app's publishing status is Testing, Google issues refresh tokens that expire after 7 days. When that happens the server returns an error saying so; run
blogger-mcp-authagain. See below for why leaving Testing is harder than it looks.
About the 7-day expiry
Testing status also means only accounts listed as test users on the consent screen can authorize at all (up to 100 of them), so add anyone who needs access there.
Escaping the 7-day expiry means publishing the consent screen to Production, and Google requires an app homepage and a privacy policy to do that. Those have to be on a domain you own and have verified in Search Console — Google explicitly rules out platforms where you cannot prove you own the subdomain, which excludes a Blogger blog, GitHub Pages, Google Sites and similar. The Blogger scope is broad enough to count as sensitive, so a published app also shows a "Google hasn't verified this app" warning until it passes review, which wants a demo video and takes days to weeks.
Google's documentation ties the 7-day expiry specifically to Testing status, which implies publishing alone is enough. Reports from developers of apps that are in Production but unverified are mixed, and this project has not tested it. Do not assume publishing will fix it.
For a personal setup, re-authorizing weekly is usually less work than owning a domain to satisfy a review process. If you do want to go through with it, you need a real domain, a page describing the app, and a privacy policy on that same domain.
2. Authorize (once)
blogger-mcp-auth
This opens a browser, asks you to sign in and consent, then caches the token at
~/.config/blogger-mcp/token.json (mode 0600). It finishes by printing the
blogs on the account together with their blog ids — you will need one of
those ids for every call.
Re-run it any time you need to re-authorize or switch account.
The consent flow lives in this separate command rather than in the server on purpose: opening a browser from inside the server would block the stdio transport, and the client would just see a server that never responds.
3. Register the server with your MCP client
Claude Code:
claude mcp add blogger -- /path/to/blogger/.venv/bin/blogger-mcp
Or in a client config file (Claude Desktop's claude_desktop_config.json):
{
"mcpServers": {
"blogger": {
"command": "/path/to/blogger/.venv/bin/blogger-mcp"
}
}
}
The server never opens a browser. If the cached token is missing or broken it
returns an error telling you to run blogger-mcp-auth.
Configuration
All optional:
| Variable | Default | Purpose |
|---|---|---|
BLOGGER_MCP_HOME | ~/.config/blogger-mcp | Directory for credentials |
BLOGGER_MCP_CLIENT_SECRETS | $BLOGGER_MCP_HOME/client_secret.json | OAuth client JSON |
BLOGGER_MCP_TOKEN | $BLOGGER_MCP_HOME/token.json | Cached token |
BLOGGER_MCP_READONLY | unset | Set to 1 to request the read-only scope |
To manage two Google accounts, run two server instances with different
BLOGGER_MCP_TOKEN paths.
Design decisions
blog_id is always an explicit argument. The OAuth token decides which
account the server acts as; blog_id decides which blog a call touches. The
server never defaults or remembers a blog — the caller is expected to know
which blog it is operating on. list_blogs exists to discover ids, not as a
required first step.
Writes are safe by default. create_post and create_page produce drafts.
Publishing is always a separate, explicit publish_post call. The intent is
that "organize a document" and "put it on the public internet" are never the
same action — which matters more than usual when the caller is a language
model. Deletes report what actually happened — delete_post reads the post
back afterwards and says whether it went to the trash (restorable with
revert_post) or is gone for good, because that depends on the post's state at
the time. revert_post remains the way to take a post down without risking
anything.
Errors carry their remediation. Anticipated failures are raised as
ToolError so the message survives to the caller. Requests are built inside
the error handler as well as executed there, because the client library
validates enum arguments while building — a mistake there would otherwise
surface as an opaque "error executing tool".
Agent guidance (the Skill)
skills/blogger-publishing/SKILL.md is an Agent Skill describing the intended
workflows: draft → review → publish, safely updating an existing post, and what
to do about the image limitation. The MCP server gives an agent the tools;
the Skill tells it how to use them well.
The Skill is only useful alongside the MCP server — install both in whichever client you use.
Claude Code — copy or symlink the folder into your skills directory:
ln -s "$PWD/skills/blogger-publishing" ~/.claude/skills/blogger-publishing
Claude Desktop / claude.ai — build the ZIP and upload it:
./build-skill.sh # writes dist/blogger-publishing.zip
Then in Claude: Settings → Capabilities → enable code execution (skills
require it), then Customize → Skills → + and upload
dist/blogger-publishing.zip.
The ZIP deliberately contains blogger-publishing/ as its root entry, which is
the structure the uploader expects. Note that description: in the frontmatter
is capped at 200 characters — keep it under that if you edit it, or the upload
is rejected.
License
MIT — see LICENSE.