blogger-mcp
An MCP server for managing a Blogger blog through the Blogger API v3 — posts, static pages, comments, blogs, and pageviews.
Blogs (read-only): list the account's blogs (
list_blogs) and look up one by id or URL (get_blog) to discover ablog_id.Posts: list with filters (
list_posts), fetch one in full (get_post), full-text search live posts (search_posts), create as draft (create_post), patch-edit (update_post), publish or schedule (publish_post), revert to draft (revert_post), delete (delete_post).Pages: list (
list_pages), get (get_page), create as draft (create_page), patch-edit (update_page), delete (delete_page).Comments: list across a blog or per post (
list_comments) and moderate — approve, spam, remove content, or delete (moderate_comment).Pageviews: coarse 7-day / 30-day / all-time blog counts (
get_pageviews).Content handling: accepts raw HTML or plain text (text is escaped and wrapped in paragraphs); no Markdown, no image upload —
<img>URLs must already be public and absolute.Safety defaults: creates drafts, publishing is a separate explicit step, and deletes are treated as irreversible.
Out of scope (API limits): creating/renaming/deleting blogs, theme/template/layout/settings editing, label management, posting or replying to comments, and per-post custom metadata.
Provides tools for managing Blogger blogs via the Blogger API v3, including listing and searching posts, creating drafts, editing, publishing, scheduling, reverting posts, managing static pages, and moderating comments.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@blogger-mcp Publish my draft post titled 'Hello World' on my blog."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
blogger-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.
Related MCP server: Blogger MCP Server
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/bloggerAdd 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-authThis 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-mcpOr 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 |
|
| Directory for credentials |
|
| OAuth client JSON |
|
| Cached token |
| unset | Set to |
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-publishingClaude Desktop / claude.ai — build the ZIP and upload it:
./build-skill.sh # writes dist/blogger-publishing.zipThen 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.
Available Tools
18 toolscreate_pageA
Create a static page. Creates a DRAFT by default.
Same content rules as create_post: raw HTML or plain text, no Markdown,
no image upload.
Inputs: title (required), blog_id (required), content (required), draft, +1 more. Returns: object
create_postA
Create a post. Creates a DRAFT by default.
Publishing is a separate, explicit step: call publish_post afterwards.
Only set draft=False when a human has asked for immediate publication.
content_format="html" sends content through untouched — Blogger stores
post bodies as raw HTML. content_format="text" escapes the text and wraps
it in paragraphs. There is no Markdown support and no image upload: any
in the HTML must already point at a public absolute URL.
Blogger cannot store per-post metadata of your own (its customMetaData
field silently discards writes), so there is no way to record which local
file a post came from. Track that outside this server if you need it.
Inputs: title (required), blog_id (required), content (required), draft, +3 more. Returns: object
delete_pageBDestructiveIdempotent
Delete a static page. Treat this as irreversible (see delete_post).
Inputs: blog_id (required), page_id (required), permanent. Returns: object
delete_postADestructiveIdempotent
Delete a post. Treat this as irreversible and confirm with a human first.
useTrash is sent unless permanent=True, but it buys nothing you can act
on: a deleted post returns 404 and never shows up in a SOFT_TRASHED listing
(verified against the live API). Whether Blogger's own web UI can still
restore it is not something the API can tell you.
If the goal is to take a post off the public blog, use revert_post
instead — it keeps the content and is trivially undone by publishing again.
Inputs: blog_id (required), post_id (required), permanent. Returns: object
get_blogARead-only
Look up one blog by its id, or by its public URL.
Pass exactly one of blog_id or url (e.g. "https://example.blogspot.com/").
Inputs: url, blog_id. Returns: object
get_pageARead-only
Fetch one static page in full, including its HTML body.
Inputs: blog_id (required), page_id (required). Returns: object
get_pageviewsARead-only
Blog-level pageview counts.
Blogger only exposes three coarse buckets and the numbers are approximate — treat them as a rough signal, not analytics. There is no per-post breakdown.
Inputs: blog_id (required), time_range. Returns: object
get_postARead-only
Fetch one post in full, including its HTML body and sync metadata.
Always call this before update_post so you are editing against the
current content rather than a stale copy.
Inputs: blog_id (required), post_id (required). Returns: object
list_blogsARead-only
List the blogs the authorized Google account can access.
Use this to discover a blog_id when you do not already have one. Every
other tool requires the caller to pass blog_id explicitly.
Inputs: no named parameters. Returns: object
list_commentsARead-only
List comments on one post, or across the whole blog if post_id is omitted.
Use status="PENDING" to find comments waiting for moderation.
Inputs: blog_id (required), status, post_id, page_token, +1 more. Returns: object
list_pagesARead-only
List the static pages of a blog (About, Contact, ...).
Pages differ from posts: no labels, no dates in the archive, and they do not appear in the blog's post feed.
Inputs: blog_id (required), status, page_token, max_results, +1 more. Returns: object
list_postsARead-only
List posts in a blog.
status filters to LIVE / DRAFT / SCHEDULED / SOFT_TRASHED (omit for all).
labels is a comma-separated list. start_date/end_date are RFC 3339
timestamps. Bodies are omitted by default — pass fetch_bodies=True only
when you actually need the HTML, since post bodies are large.
Inputs: blog_id (required), sort, labels, status, +6 more. Returns: object
moderate_commentADestructiveIdempotent
Moderate one comment.
approve— publish a pending comment (also un-spams it)spam— mark as spam and hide itremove_content— blank the text but leave the "comment removed" stubdelete— remove it entirely; this one is NOT recoverable, so confirm with the human first
The Blogger API cannot create comments, so replying to a commenter has to be done by hand in the Blogger UI.
Inputs: action (required), blog_id (required), post_id (required), comment_id (required). Returns: object
publish_postAIdempotent
Publish a draft post, making it publicly visible.
This is the irreversible-feeling step — confirm with the human before calling it unless they already asked for publication.
Pass publish_date as an RFC 3339 timestamp (e.g. "2026-09-10T09:00:00Z")
to schedule publication for a future time instead of publishing now; the
post then sits in SCHEDULED status until then.
Inputs: blog_id (required), post_id (required), publish_date. Returns: object
revert_postAIdempotent
Take a live or scheduled post back to DRAFT, removing it from the blog.
The content is kept; only its visibility changes. This is the safe way to pull a post down.
Inputs: blog_id (required), post_id (required). Returns: object
search_postsARead-only
Full-text search over the published posts of a blog.
Note this only covers live posts; drafts are not indexed. To find a draft,
use list_posts with status="DRAFT".
Inputs: query (required), blog_id (required), fetch_bodies. Returns: object
update_pageBDestructiveIdempotent
Update a static page, sending only the fields you pass (PATCH semantics).
Set publish=True to publish a draft page as part of the edit.
Inputs: blog_id (required), page_id (required), title, content, +2 more. Returns: object
update_postADestructiveIdempotent
Update a post, sending only the fields you pass (PATCH semantics).
Omitted fields are left untouched, so you can change just the title without resending the body. Two things to know:
labelsREPLACES the whole label list. To add one label, read the current list withget_postfirst and pass the full new list.publish=False(the default) does not unpublish anything — a live post stays live. It only means "do not publish a draft as part of this edit". Userevert_postto take a live post back to draft.
Inputs: blog_id (required), post_id (required), title, labels, +4 more. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 18 tools
Each tool targets a distinct resource and action: blogs, posts, pages, comments, and pageviews are cleanly separated. The post lifecycle tools (create/update/publish/revert/delete) are especially well-delineated, with descriptions clarifying the overlap between revert and delete.
Tool names follow a consistent verb_noun pattern: list_*, get_*, create_*, update_*, delete_*, plus a few action-specific verbs like publish_post, revert_post, and moderate_comment. The pattern makes the set predictable and easy to navigate.
18 tools is slightly above the typical sweet spot, but each tool covers a distinct Blogger resource or lifecycle step. The count is justified by the breadth of the domain: posts, pages, comments, blogs, and pageviews.
The post lifecycle is fully covered, and pages and comments have solid coverage. Minor gaps exist, such as no way to unpublish a page and no comment creation, but the API limitations are explicitly documented and agents can work around them.
Maintenance
Related MCP Connectors
Create, edit, organize, publish, and configure JustBlogged blogs from MCP clients.
Hosted MCP for BlogBat: read, write, generate, and publish blog articles and content.
Hosted MCP for BlogBat: read, write, generate, and publish blog articles and content.
Blog MCP server for AI agents. One signup call returns a blog, an API key and a live URL; then your agent publishes markdown posts and gets public URLs back. Your agent does the writing; you get a dashboard for stats.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables LLMs to automate Google Blogger content management by providing tools for single and batch post creation via the Blogger API. It supports secure OAuth2 authentication and allows for seamless blog integration through the Model Context Protocol.MIT
- AlicenseAqualityDmaintenanceEnables AI assistants to interact with the Google Blogger API v3 to manage blog posts and metadata. It supports the full post lifecycle including creating, updating, publishing, and deleting content through natural language.1025 npm1MIT
- AlicenseAqualityDmaintenanceA custom MCP server for interacting with Google Blogger blogs. It provides tools to list, create, edit, delete, and publish blog posts through Claude Code or Claude Desktop.9MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI models to interact with Google Blogger blogs, manage posts, labels, and retrieve blog information via API key or OAuth2.20 npmMIT