Building a Ghost CMS MCP Server: When Your Blog Finally Gets the AI Assistant It Deserves
I built an MCP server for Ghost CMS that lets you manage your entire blog through conversation with Claude. Here's how it works, why it matters, and what's coming next.

Remember when managing a blog meant juggling multiple browser tabs, copying and pasting between tools, and silently cursing at WordPress for the hundredth time?
Well, I just built something that might change that game entirely – and honestly, it feels a bit like giving your blog superpowers.
I recently vibe-coded an MCP (Model Context Protocol) server for Ghost CMS, and the experience has been nothing short of transformative. If you've ever wished you could just talk to your blog and have it actually listen, this might be exactly what you've been waiting for.

Standing on the Shoulders of Giants (Or at Least Really Smart People)
Before getting too carried away with my brilliance, I need to give credit where it's due. The initial inspiration came from Fanyang Meng's excellent post about building a Ghost MCP server in TypeScript.

Fanyangs excelent post that got the wheel spinning
Fanyang's Excelent Github Repo
While I went full "vibe coding" mode and built my own Python version from scratch (because I enjoy making things harder for myself), Fanyang's work showed me what was possible and got my creative wheels spinning. Sometimes the best way to learn something new comes from seeing someone else's solution and thinking, "Hmm, what if I tried to do that?"
What the Heck is MCP Anyway?
Let me paint you a picture of what MCP actually is, because once it clicks, you'll see why this is such a game-changer.
Imagine you're at a party where everyone speaks different languages. You've got your blog (speaking Ghost API), your analytics dashboard (speaking Google Analytics), your email service (speaking whatever Mailchimp speaks), and you're trying to coordinate all these systems to work together. Normally, you'd need to learn each language and constantly translate between them.
MCP is like having a brilliant polyglot friend who can talk to everyone at the party and coordinate the whole thing. But here's the beautiful part: You don't need to learn any of those languages! You just talk to your LLM friend in plain English, and they handle all the coordination.
Documentation for Model Context Protocol
How MCP Communication Actually Works
The magic happens through something called "tools". In MCP, a tool is essentially a superpower you give to Claude. When I built my Ghost MCP server, I created tools like:
- create_post - "Hey Claude, write a post about our new feature"
- schedule_post - "Schedule this for next Tuesday at 9 AM"
- find_members - "Show me everyone who joined last month"
- manage_tags - "Tag this with 'product updates' and 'big news'"
Here's what happens when you say, "Create a post about AI trends and schedule it for tomorrow":

- You speak human: "Create a post about AI trends and schedule it for tomorrow"
- Claude understands context: It breaks this down into: write content + create post + schedule for specific time
- MCP server translates: Converts this into Ghost API calls with proper authentication
- Ghost executes: Your blog actually creates and schedules the post
- Claude reports back: "Done! Your post 'The Future of AI in 2025' is scheduled for tomorrow at 9 AM"
The beautiful thing? You had a conversation. Not a series of API calls, not multiple browser tabs, not copy-pasting between systems. Just a conversation.
The Anatomy of an MCP Tool
Let me show you what one of these "tools" actually looks like under the hood, because it's surprisingly elegant:
# --- Posts Tools ---
@mcp.tool()
async def add_post(
title: str,
html: str,
status: str = "draft",
source: str = "html",
authors: Optional[List[Dict[str, str]]] = None,
tags: Optional[List[Dict[str, str]]] = None,
featured: Optional[bool] = False,
feature_image_url: Optional[str] = None, # URL for the feature image
feature_image_alt: Optional[str] = None, # Alt text for the feature image
custom_excerpt: Optional[str] = None,
slug: Optional[str] = None,
published_at: Optional[str] = None, # ISO 8601 date-time string, e.g., "2024-12-31T10:00:00.000Z"
email_only: Optional[bool] = False, # ADD THIS LINE
newsletter: Optional[str] = None, # ADD THIS LINE
email_segment: Optional[str] = None # ADD THIS LINE
) -> str:
"""
Create a new post in Ghost CMS.
Args:
title: The title of the post. This is a required field.
html: The full HTML content for the body of the post.
This is where the main article text, paragraphs, headings, images (using <img> tags with src URLs),
and other HTML formatting should be placed. Ensure this is well-formed HTML.
status: The status of the post. Options[cite: 1]:
'draft' (default): Saves the post as a draft, not publicly visible.
'published': Publishes the post, making it live on your site.
'scheduled': Schedules the post to be published at a future date.
If 'scheduled', the 'published_at' field must also be provided. [cite: 1]
To publish immediately, ensure this is set to 'published'.
source: Optional. Format of the content being provided. Options:
'html' (default): Treats the html parameter as HTML to be converted to Lexical.
'lexical': Treats the html parameter as raw Lexical JSON (advanced usage).
authors: Optional. List of author objects (e.g., [{"id": "author_id_1"}] or [{"email": "author@example.com"}]).
Assigns authors to the post.
tags: Optional. List of tag objects (e.g., [{"name": "Tech"}, {"slug": "news"}] or [{"id": "tag_id_1"}]).
Assigns tags to the post.
featured: Optional. Boolean (true/false) to mark the post as featured. Defaults to false.
feature_image_url: Optional. URL of an image to be used as the post's feature image.
feature_image_alt: Optional. Alt text for the feature image.
custom_excerpt: Optional. A short summary or excerpt for the post.
slug: Optional. Custom URL slug for the post (e.g., 'my-awesome-post').
If not provided, Ghost generates it from the title.
published_at: Optional. An ISO 8601 date-time string (e.g., "YYYY-MM-DDTHH:MM:SS.sssZ").
Required if status is 'scheduled'. This sets the time for the post to go live. [cite: 1]
email_only: Optional. Boolean (true/false) to send as email-only without publishing on site.
Requires 'newsletter' parameter and status must be 'published' or 'scheduled'.
newsletter: Optional. Newsletter slug to send the post through (required if email_only=True).
Example: 'weekly-newsletter' or 'default-newsletter'.
email_segment: Optional. NQL filter for member segmentation. Examples:
'status:free' (free members only), 'status:-free' (paid members only),
'all' (all members). Defaults to 'all' if not specified.
"""
post_data: Dict[str, Any] = {"title": title, "html": html, "status": status}
if authors:
post_data["authors"] = authors
if tags:
post_data["tags"] = tags
if featured is not None: # Check for None explicitly if False is a valid value
post_data["featured"] = featured
if feature_image_url: # Ghost expects "feature_image" not "feature_image_url"
post_data["feature_image"] = feature_image_url
if feature_image_alt:
post_data["feature_image_alt"] = feature_image_alt
if custom_excerpt:
post_data["custom_excerpt"] = custom_excerpt
if slug:
post_data["slug"] = slug
if published_at:
post_data["published_at"] = published_at
if email_only is not None: # ADD THIS
post_data["email_only"] = email_only # ADD THIS
# Validate: if status is 'scheduled', published_at must be provided.
if status == "scheduled" and not published_at:
return json.dumps({
"error": "Validation Error: 'published_at' is required when status is 'scheduled'. Please provide a future ISO 8601 date-time.",
"tip": "Example format for published_at: '2024-12-31T10:00:00.000Z'"
}, indent=2)
if email_only and not newsletter:
return json.dumps({
"error": "Validation Error: 'newsletter' is required when 'email_only' is True.",
"tip": "Provide the newsletter slug, e.g., 'weekly-newsletter' or 'default-newsletter'"
}, indent=2)
if email_only and status not in ["published", "scheduled"]:
return json.dumps({
"error": "Validation Error: 'status' must be 'published' or 'scheduled' when 'email_only' is True.",
"tip": "Email-only posts need to be published or scheduled to be sent"
}, indent=2)
payload = {"posts": [post_data]}
# Build endpoint with parameters
endpoint = "/posts/"
params = []
if source == "html":
params.append("source=html")
if newsletter:
params.append(f"newsletter={newsletter}")
if email_segment:
params.append(f"email_segment={email_segment}")
if params:
endpoint += "?" + "&".join(params)
result = await make_ghost_api_request("POST", endpoint, payload=payload)
return json.dumps(result, indent=2)
Model Context Protocol Tool for Creating a post written in Python.
Okay, this looks like a lot, but it's actually telling a beautiful story about what's possible. When Claude sees this function, it's like handing someone a really well-organized toolbox where every tool has a clear label.
The required parameters (title
and html
) are the non-negotiables. But look at all those Optional
parameters. Each one is Claude understanding another dimension of what you might want to do with your content.
When you say "write a post about coffee culture and make it really stand out," Claude spots that featured
parameter and thinks "ah, they want this highlighted." Say "schedule it for next week" and Claude connects that to published_at
, automatically formatting the date properly.
Here's what blew my mind: Claude doesn't just fill in parameters randomly. It understands relationships through your documentation of the function. If you ask for an email-only post, Claude knows to check for the newsletter parameter.
It's like having a really good assistant who doesn't just follow instructions but understands the underlying logic.
The @mcp.tool()
decorator is what makes this magic happen. Without it, this is just Python code sitting in a file. With it, Claude looks at this and thinks "I now have the power to create sophisticated blog posts with scheduling, targeting, and distribution control."
Why This Feels Like Magic (But Isn't)
Here's what genuinely blew my mind: Claude doesn't just execute commands, it understands context.
Say you're having a conversation about your content strategy, and midway you mention "Oh, and we should probably write about that new feature we launched." Claude remembers the context and can write the post, use appropriate tags, and even reference previous conversations.
It's like having a content manager who:
- Never forgets what you talked about yesterday
- Understands your brand voice and content strategy
- Can execute across multiple platforms seamlessly
- Learns your preferences over time
The MCP server handles all the technical plumbing – authentication tokens, API rate limits, error handling, data formatting – while Claude focuses on understanding what you actually want to accomplish.
What This Actually Feels Like in Practice
Let me give you a real example of how this changes your workflow. Here's the old way vs. the new way:
The Old Dance:
- Open Ghost admin panel
- Click "New Post."
- Write your content
- Switch to another tool to check analytics for optimal posting time
- Come back to add tags
- Schedule publication
- Set up email distribution to the right subscriber segments
- Pray you didn't miss anything
The New Way: You: "Hey Claude, write a post about the productivity techniques we discussed yesterday. Make it conversational but actionable, tag it with 'productivity' and 'tips', and schedule it for when our audience is most active. Send it to subscribers who engage with productivity content."
Claude: "Got it! I'll create a post about the productivity techniques from our discussion. Let me write something engaging, schedule it for Thursday at 10 AM when your audience is most active, and send it to subscribers who've engaged with productivity content in the past."
And it happens. All of it. While you grab coffee.
The Real Benefits (Beyond the Cool Factor)
1. Workflow Compression
What used to take 5 hours and 6 different clicks now happens in seconds. Your creative momentum stays intact.
2. Content Strategy Integration
Because Claude understands context, it can help you maintain consistency across your content strategy. "Write a follow-up to last week's post about user feedback" – and it knows exactly what you're referring to.
3. Community Management at Scale
Managing hundreds or thousands of subscribers becomes conversational. "Show me members who joined last month but haven't engaged with any emails" – boom, done.
4. Scheduling That Thinks
Instead of manually calculating optimal posting times, you can ask for posts to be scheduled "when our European audience is most active" and let the system figure out the details.
What's Next? (The Tentative Roadmap - But I Want Your Input!)
This is just the beginning, and honestly, I'm pretty excited about where this could go. That said, this roadmap is very much a work in progress, and I'd love to hear what you think would be most valuable. Here's what I'm considering:
Content Analytics Integration with GA4
"How did my last three posts perform compared to my usual engagement rates?" – but taken to the next level. I'm planning to build an MCP protocol integration with Google Analytics 4, so you could get automatic insights, performance comparisons, and even suggestions for improvement, all through natural conversation.
News API Integration for Automated Newsletters
Picture this: "Create this week's marketing newsletter with the top industry trends" – and having it automatically pull from various news APIs, curate the most relevant stories for your audience, and draft a cohesive newsletter. No more Sunday night scrambling to put together your weekly roundup.
Advanced Automation Workflows
Think "if a post gets more than 100 likes in the first hour, automatically promote it to featured status and send a follow-up email to premium subscribers."
Cross-Platform Publishing
Why stop at Ghost? The same MCP approach could simultaneously handle LinkedIn, Twitter, Medium, and your newsletter platform.
Want to Try It Yourself?
I haven't made my version of the Python MCP protocol publicly available yet. I might release it soon, so sign up and subscribe to get the notification straight to your inbox.
If you can't wait, Fayang Mengs Typescript repo is publicly available!
The Bigger Picture
What excites me most isn't just the technical achievement (though I'm pretty proud of getting Ghost's webhook system to play nicely with MCP). It's what this represents for the future of how we interact with our tools.
We're moving toward a world where the friction between having an idea and executing on it approaches zero. Where your tools understand not just what you want to do, but why you want to do it, and can help you do it better.
This Ghost MCP server is one small step in that direction, but it feels like a pretty significant one to me.
Have you experimented with MCP servers for your own tools? I'd love to hear about your experience – the successes, the failures, and especially the weird edge cases that made you question your life choices. Drop me a line, and let's compare notes on building the future of human-AI collaboration.