Launching content-distribution-mcp: one finished post, eight channels
One finished post fans out to DEV.to, Hashnode, GitHub Discussions, Bluesky, Reddit, Medium, LinkedIn, and Twitter from any MCP host.
TL;DR: Install content-distribution-mcp, point any MCP host at it, and route one finished post to eight developer-community channels with idempotent state, per-subreddit anti-spam pre-flight, and dual YAML or Notion backends.
Most "cross-posting" tooling is a Buffer paraphrase: schedule a calendar, click eight times, hope nobody double-posts. The pieces that actually bite an indie publisher are different. You need an idempotency key that survives a network blip so a retry does not produce two DEV.to articles. You need a Reddit gate that refuses the post before a mod sees it, not after. You need to plug the whole thing into whichever AI host you happen to use this quarter. content-distribution-mcp is the open-source Model Context Protocol server we wrote to do exactly that, and we shipped it on PyPI and GitHub today.
Why we built this
We publish the same post across DEV.to, Hashnode, GitHub Discussions, Bluesky, Reddit, Medium, LinkedIn, and Twitter. The first version of that pipeline was a Python script. The second was an n8n graph. Both broke in the same three places: a network blip created a duplicate, a Reddit submission got removed by Automoderator because the account had posted to the same subreddit two hours earlier, and the credentials lived in three places at once.
We also wanted the publishing step to be callable from whatever harness we used at the time. Today that is Claude Code; six months ago it was a plain Python loop; tomorrow it will probably be something else. MCP is the protocol that lets a tool stay portable across those hosts, so we wrote the distribution server against it from the start. The sibling pieces in our stack, the n8n MCP server and the AI-SEO MCP, follow the same rule.
What it is
A model-agnostic MCP server that takes a finished piece of content and routes it to developer-community platforms with idempotent state management, per-subreddit anti-spam rules, and dual Notion or YAML backends.
The server makes no LLM calls of any kind. There is no anthropic import anywhere in src/. All copy transformation is the caller's responsibility; the MCP hands back per-channel constraints via the hints() tool and the agent decides what to do with them. That is what "model-agnostic" actually means here, not a marketing label.
It runs unchanged under Claude Code, Claude Desktop, Cursor, n8n via the MCP Client node, and plain Python with the mcp client library. Anything that speaks MCP over stdio or SSE works. The host process supplies credentials and host-generated Variant text; the MCP supplies idempotent I/O.
Architecturally there are three layers. The agent layer reads source content, generates per-channel copy (the LLM work lives there), and calls MCP tools. The MCP server layer is pure I/O with no LLM calls: adapters, state, idempotency, scheduling, retries. The backend layer stores Distribution Profiles, the Subreddit Catalog, the Post Log, and the Scheduled Queue. The boundary matters because it is what keeps the server useful when you swap models or change harnesses.

Tool surface: eight tools, no more
Eight tools, each with a single job. Full docstrings live in spec.md.
| Tool | Purpose |
|---|---|
publish | Immediate publish; idempotent on (content.id, variant.channel) |
schedule | Queue variants for a schedule_at timestamp |
drain | Fire any due scheduled posts (cron-friendly, one-shot) |
status | Per-variant state for a content piece |
unpublish | Best-effort delete (DEV.to and GitHub Discussions only) |
hints | Static per-channel metadata: char limits, tag vocabulary, canonical-URL support |
list_profiles | Configured Distribution Profiles |
list_subreddits | Curated Subreddit Catalog entries |
The load-bearing detail is idempotency. Every publish call keys on (content.id, variant.channel). If the network drops between "Hashnode returned 200" and "we wrote the state row", a retry sees the existing successful state and returns the same URL instead of posting a second article. The state lives in whichever backend you wired in, which is the next section.
schedule queues a variant with a schedule_at timestamp. drain is the cron-friendly one-shot that fires anything due and exits, so you can run it from a system cron or a GitHub Action without keeping a long-lived process alive. hints returns static per-channel metadata so the agent can shape copy correctly: DEV.to accepts up to four tags from a curated vocabulary; Bluesky posts cap at 300 graphemes; GitHub Discussions does not have a canonical-URL field and needs a footer instead. None of that lives in the agent's prompt; it lives in the MCP and gets queried on demand.
Eight channels, three tiers
| Channel | Tier | Notes |
|---|---|---|
| DEV.to | Auto | Forem API v1, native canonical_url |
| Hashnode | Auto | GraphQL, native originalArticleURL |
| GitHub Discussions | Auto | GraphQL per-repo, footer for canonical (no native field) |
| Bluesky | Auto | atproto SDK, canonical link appended to post text |
| Auto-gated | Per-subreddit cooldown, 5/day global cap, self-promo ratio, flair resolution | |
| Medium | Manual (browser) | Playwright pre-fill plus batched-tab UX, mark-live CLI |
| Manual (browser) | Personal feed and company-admin compose, plain-text draft, mark-live CLI | |
| Twitter / X | Manual (browser) | Free-tier API unusable; plain-text draft and compose URL, mark-live CLI |
The "manual (browser)" tier is for hostile or dead APIs. LinkedIn limits compose to specific partners. Medium has no v2 API. Twitter's free tier is effectively unusable for posting. For those three, the adapter writes the draft into a Playwright-driven browser session, opens the compose UI pre-filled, and waits for the human to hit Publish. The mark-live CLI then records the live URL back into state, so status() still returns the truth.

mark-live CLI keeps state honest even when the publish step is a human click.The Reddit gate
Reddit is the part of cross-posting that ruins reputations. Two posts to r/programming in the same week get the account flagged. A self-promo ratio above 10% gets the user shadowbanned from a sub they did not even know had a rule. Most "schedulers" learn this the slow way, by mod removal.
The Reddit adapter refuses to post if any of these checks fail:
- Per-subreddit cooldown. Configured per sub in the Subreddit Catalog (default 7 days). Querying for prior submissions by the same account is part of pre-flight.
- 5-per-day global cap. Counts across all subs in the catalog, not per-sub. Tunable.
- Self-promo ratio. Walks recent activity and refuses if non-self-promo submissions and comments fall below the configured ratio.
- Flair resolution. If the sub requires a flair and none is supplied, the adapter fetches the available flairs, fails fast, and returns the list so the caller can pick one.
All four checks run before the submission hits the wire. If any fail, you get a structured error back, the post does not happen, and your karma stays intact. The catalog ships with sensible defaults for the developer-adjacent subs we publish into; edit the YAML or the Notion database to add your own.

YAML or Notion: pick one
State lives in a backend that implements the StateBackend protocol. Two ship in the box.
YamlBackend stores four YAML files in ~/.distribution-mcp/: Distribution Profiles, Subreddit Catalog, Post Log, and Scheduled Queue. Zero config, right for solo and local use. git init the directory and version it.
NotionBackend stores the same shape across three Notion databases (Distribution Profiles, Subreddit Catalog, Post Log), with URL write-back to the source task on success. Right for team and agency use. The provisioner builds the three databases for you:
content-distribution-mcp provision-notionThe MCP picks the backend from a constructor argument. Caller code does not change when you swap them, which means you can prototype on YAML on your laptop and move the same workflow into a Notion-based agency board later without rewriting anything.
Install
Requires Python 3.11 or later.
pip install content-distribution-mcpBrowser fallback extras for Medium, LinkedIn, and Twitter pre-fill:
pip install content-distribution-mcp[browser]
playwright install chromiumBluesky extras:
pip install content-distribution-mcp[bluesky]Wire it into your MCP host. For Claude Code, drop this into .claude/mcp.json:
{
"mcpServers": {
"content-distribution": {
"command": "content-distribution-mcp",
"args": ["serve"]
}
}
}For n8n, install the MCP Client node and point it at content-distribution-mcp serve over stdio. For plain Python, use the mcp client library and call publish directly. The repo README has runnable examples for every host.
How we use it ourselves
This post is publishing itself through the server we are announcing. The Ghost draft you are reading was written by a Claude Code skill, the four auto-tier variants (DEV.to, Hashnode, GitHub Discussions, Bluesky) are queued for the same content ID, and the four manual-tier variants (Reddit x5, LinkedIn, Twitter) sit behind the browser adapter waiting for a human click. The Reddit gate already refused two of the planned subreddit submissions because the cooldown had not elapsed, which is exactly what is supposed to happen. State is in the YAML backend in ~/.distribution-mcp/; we will move it to Notion when the team grows past one person.
Links
- Product page: automatelab.tech/products/mcp/content-distribution-mcp/
- PyPI: pypi.org/project/content-distribution-mcp
- GitHub: AutomateLab-tech/content-distribution-mcp
FAQ
What is content-distribution-mcp?
An open-source MCP server that publishes one piece of content to eight developer-community channels: DEV.to, Hashnode, GitHub Discussions, Bluesky, Reddit, LinkedIn, Medium, and Twitter. Idempotent on (content.id, variant.channel), with per-subreddit anti-spam pre-flight and dual YAML or Notion backends. MIT-licensed.
How is this different from Buffer or Hootsuite?
Two things Buffer and Hootsuite do not do: an idempotency key that survives retries so a network blip never produces two posts, and a Reddit pre-flight that refuses the submission before a mod sees it (cooldown, daily cap, self-promo ratio, flair resolution). It is also a tool, not a SaaS; you run the server yourself and the state lives on disk or in your Notion workspace.
Does it work with non-Claude MCP hosts?
Yes. The server has zero Anthropic-specific code. It speaks standard MCP over stdio or SSE and works unchanged with Cursor, Claude Desktop, Claude Code, n8n via the MCP Client node, and plain Python with any LLM SDK (OpenAI, Anthropic, Gemini, Ollama, or none at all). Run grep -ri "anthropic" src/ to verify.
How do I install it?
Python 3.11 or later: pip install content-distribution-mcp. Add [browser] for Medium, LinkedIn, and Twitter pre-fill (then playwright install chromium), and [bluesky] for the atproto adapter. Start the stdio server with content-distribution-mcp serve and wire it into your host's MCP config.
What does it cost?
The server is free under the MIT license. You pay for whatever the channels charge: DEV.to, Hashnode, GitHub Discussions, Bluesky, and Reddit are free; Medium, LinkedIn, and Twitter post via your existing accounts. There are no hosted fees, no per-post metering, and no LLM tokens consumed by the server itself.