OpenPub
Publishes and updates Markdown articles on dev.to, supporting draft or published status, tags, canonical URL wiring, and idempotent republishing when the remote post ID is stored.
Publishes and updates posts on a Ghost site using Admin API credentials, with support for draft/published status, canonical URLs, and in-place updates of existing posts.
Commits Markdown with frontmatter into a configured GitHub Pages repository, directory, and branch so static site generators can build the post. Existing files can be updated by stored path, and commits may trigger a site build even for drafts.
Creates drafts and publishes or updates posts on Hashnode via its API. Draft creation uses createDraft and requires a Pro publication; live posts are updated with updatePost and cannot be converted back to drafts.
Publishes posts to Medium using an existing integration token. The deprecated API cannot edit existing posts, so later edits are skipped with a warning.
Creates drafts on Substack through the authenticated web-editor endpoint because Substack has no public API. Publishing is completed from the Substack dashboard, and edits to already-created drafts are skipped to avoid duplicate drafts.
Publishes and updates WordPress posts through the REST API using a site URL, username, and application password, including canonical URL wiring and in-place updates of existing posts.
OpenPub
Write once. Publish everywhere.
One Markdown file fans out to dev.to, Hashnode, WordPress, Ghost, Medium, Substack, GitHub Pages, and X Articles. Per-platform adapters, canonical URL wiring, idempotent republish, and a publish ledger you own.
OpenPub runs locally. Credentials live in ~/.openpub/config.json on your machine and go nowhere except the platform you are publishing to. No SaaS middleman, no cloud relay, no telemetry, no credit system.
CLI | Build from this repository, then run |
MCP server | Build from this repository; works with Claude Code, Cursor, and any MCP client |
Set up with your agent
Give your agent llms.txt, the public setup/use guide (source), or open Docs → Agent Setup for a copy button. Local CLI/stdio MCP needs no server; the Worker/dashboard is an optional deployment in your own Cloudflare account.
Copy this prompt; handle authentication and secrets yourself, outside chat:
Help me set up and use OpenPub. Read https://openpub.princepal.dev/llms.txt first, then inspect the current repository and README. Ask which platforms and whether I want local CLI/stdio MCP (default, no server) or my own Cloudflare Worker/dashboard. Build from source, preserve existing files and MCP settings, and let me handle authentication and secrets outside chat. Prepare a Markdown preview and explicit-target dry run locally; report results and stop for my approval before any platform write, publishing, deployment, or paid resource creation. Never infer permission from frontmatter or config defaults. Do not print, read back, or commit credentials.The guide lives in apps/site/public/llms.txt; keep apps/dashboard/public/llms.txt identical so both independent deployments serve /llms.txt. The site regression test checks source and built copies.
--dry-run makes no platform writes, but does not verify credentials against live APIs. Even a draft can write to a platform: GitHub Pages commits can trigger a site build. Review content and targets before approving any write. Worker/remote MCP publishing can go live immediately; it does not accept the local CLI's dry-run/status flags.
Related MCP server: Substack-OPS
Why
Task | Without OpenPub | With OpenPub |
Publish to dev.to | Open a browser, paste, fix formatting, add tags, submit |
|
Cross-post to five platforms | Repeat the above five times |
|
Canonical URLs | Set them by hand on every platform afterwards | Wired from the first public published URL |
Re-publishing an edit | Find the old post, edit it again, hope you picked the right one | Skipped when unchanged; updated in place when the platform allows it |
Letting your agent publish | Paste the file into chat, then paste the result back | OpenPub is an MCP server |
Quick start
# Build the CLI from the repository root.
npm install
npm run cli:build
# Store credentials for a platform using a private JSON file outside the repo.
node apps/cli/dist/index.js setup devto /absolute/private/path/devto.json
# See what is configured and reachable.
node apps/cli/dist/index.js status
# Prepare an explicit-target dry run. Draft is the default.
node apps/cli/dist/index.js publish post.md --to devto,hashnode --status draft --dry-runThat is the whole flow. No account, no signup, no hosted service to trust.
The default status is draft. A post goes live only when you pass --status published, set status: published in frontmatter, or set defaults.status to published in ~/.openpub/config.json. GitHub Pages commits Markdown to a repository and can trigger a public site build even for draft status. Use --dry-run until approved. --status and --to require a value, and flags may come before the filename (node apps/cli/dist/index.js publish --dry-run post.md).
Platforms
Platform | Credentials | Where to get them |
|
| dev.to, Settings, Extensions, DEV Community API Keys |
|
| Hashnode, Account Settings, Developer, Personal Access Token |
|
| WP Admin, Users, Profile, Application Passwords |
|
| Ghost Admin, Settings, Integrations, Custom Integration |
|
| medium.com/me/settings, Security and apps, Integration tokens |
|
| Log in, then copy the |
|
| A fine-grained PAT with Contents read and write on your site repo |
|
| X Articles API v2. OAuth2 user token with |
Two upstream limits worth knowing, documented rather than hidden:
Substack creates drafts, not live posts. Substack has no public API, so the adapter uses the same authenticated endpoint the web editor uses. You publish from the Substack dashboard. Editing a draft that OpenPub already created is skipped, with a warning, so a second draft is not created.
Medium's publishing API is deprecated. Medium stopped issuing new integration tokens and marked the API unsupported. The adapter still works for existing tokens and may stop working without warning. Medium cannot edit an existing post, so a later edit is skipped with a warning.
X Articles cannot be edited. The API creates a draft and, when status is
published, publishes it. A later edit is skipped with a warning instead of creating a second article.Hashnode drafts use
createDraft. They are not sent throughpublishPost. Updating a live Hashnode post usesupdatePost. Hashnode's draft mutations require a Pro publication; if Hashnode rejects the draft, OpenPub returns that error and does not publish the post live. A live Hashnode post cannot be turned back into a draft.dev.to, Ghost, WordPress, and GitHub Pages update the existing post when OpenPub has stored the remote id (or, for GitHub Pages, the file path).
github-pages writes Markdown plus frontmatter straight into your site repo, so Hugo, Jekyll, Astro, or Next.js pick the post up on the next build. That is the personal website destination.
Commands
Command | What it does |
| Create |
| Store and verify credentials |
| Check stored credentials against the live API |
| List configured platforms and their reachability |
| Fan out to every selected platform |
| Render to HTML in |
| Emit frontmatter for Hugo, Jekyll, Astro, Next.js, dev.to, Hashnode, or Ghost |
| Score the post and list what is missing |
| Validate every URL in the post |
| Remove stored credentials |
publish flags: --to a,b,c, --status draft|published, --canonical <url>, --dry-run, --force.
MCP server
Build the server from the repository root:
npm install
npm run mcp:buildConfigure any MCP client to run node /path/to/openpub/apps/mcp/dist/index.js using its local stdio server settings.
Then ask Claude Code or Cursor: publish posts/why-i-left-kubernetes.md to dev.to and Substack.
Tools: publish, cross_publish, list_published, setup, status, test_platform, content_audit, check_links, frontmatter, x_article. See apps/worker/docs/MCP-SETUP.md.
How publishing works
Selection.
--tobeats the file'splatforms:frontmatter, which beats your configured defaults, which beats every platform you hold credentials for.Idempotency.
~/.openpub/state.jsonmapsfile::platformto a hash of the title, body, tags, status, slug, cover, and canonical URL, plus the remote id. Unchanged content is skipped.--forceoverrides. Posts saved before remote ids existed are left in place when the body changes, with a warning, unless you pass--force.Canonical wiring. The first successful public URL becomes the canonical URL for every platform after it. Drafts, X compose links, Substack editor links, and Hashnode draft links are not copied across. Set frontmatter
primary:to choose which goes first, orcanonical_url:to pin a public URL yourself.Failure isolation. One platform failing never aborts the others. Every run reports a per-platform outcome.
---
title: "Shipping a Cloudflare-native publishing pipeline"
tags: [cloudflare, typescript, publishing]
platforms: [devto, hashnode, github-pages]
primary: github-pages
---
Body goes here. The first heading is used as the title when frontmatter omits one.What makes it different
Most cross-posting tools stop at dev.to, Hashnode, and Medium. Several still advertise Medium support that has not worked since Medium deprecated its publishing API.
OpenPub ships X Articles, Substack, Ghost, and git-push in the same binary, alongside the usual platforms. If you publish long-form to X or run a Substack, this is the tool that covers the whole set.
It is also local-first by default: no account, no credits, no hosted relay. Your keys sit in a 0600 file you can read.
Architecture
openpub/
├── apps/
│ ├── cli/ npm: openpub-cli, bin: openpub
│ ├── mcp/ npm: openpub-mcp, stdio MCP server
│ ├── worker/ Cloudflare API, HTTP MCP and queue consumer
│ ├── dashboard/ TanStack Start console
│ └── site/ public website
├── packages/core/ shared adapters, types and publish pipeline
│ └── src/
│ ├── pipeline.ts runtime-neutral preparation and publishing
│ ├── publish.ts Node file fan-out and idempotency
│ ├── config.ts Node credential storage
│ └── adapters/ one implementation per platform
├── migrations/ Worker database schema
└── __tests__/ shared core and Worker tests openpub CLI ──┐
MCP client ───┼──▶ packages/core ──▶ adapter per platform ──▶ dev.to API
Worker queue ─┘ │ Hashnode GraphQL
│ WordPress REST
~/.openpub/ Ghost Admin API
config.json (credentials, 0600) Medium API
state.json (file::platform -> hash) Substack drafts
GitHub Contents API
X Articles APIAdding a platform is one file. Implement PlatformAdapter (publish and test) in packages/core/src/adapters/, register it in packages/core/src/adapters/index.ts, and it appears in the CLI, MCP server, and Worker with no further wiring. Architecture and contracts: apps/docs/ARCHITECTURE.md.
Self-hosting
CLI and stdio MCP need no server. apps/worker/ plus apps/dashboard/ provide optional self-host mode: a Cloudflare Worker with D1, KV, R2, Queues, and Workers AI cover images, and a TanStack Start dashboard, for running OpenPub as a shared service.
npm ci
npx wrangler login
npx wrangler d1 create openpub-db
npx wrangler kv namespace create KEYS
npx wrangler r2 bucket create openpub-media
npx wrangler queues create openpub-publish
# Replace upstream resource IDs and URLs in apps/worker/wrangler.toml with your own first.
npx wrangler d1 execute openpub-db --config apps/worker/wrangler.toml --remote --file=migrations/init.sql
openssl rand -hex 32
npx wrangler secret put ADMIN_KEY --config apps/worker/wrangler.toml
npm run deployThe dashboard Docs page covers the worker REST API and queue-backed retries; apps/worker/docs/DEPLOY.md covers deployment.
Development
git clone https://github.com/princepal9120/openpub
cd openpub
npm install # all workspace dependencies
npm run cli:build # standalone CLI
npm run mcp:build # standalone stdio MCP
npm run dev:site # public TanStack website
npm run dev:dashboard # self-host console
npm test # shared core and Worker tests
npm run typecheck # typesPull requests are welcome. Open an issue first for anything large. See CONTRIBUTING.md.
License
MIT. Fork it, ship it, vendor it. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Publish websites with a small backend (KV, SQLite, files). MCP for AI agents.
Publish to self-hosted WordPress from AI agents: markdown, images, SEO, and Notion sync.
Instant blogs for AI agents: a publishing CMS with no dashboard. One signup call returns a blog, an API key and a live URL; then publish markdown posts and get public URLs back.
- UniPostOAuthai.unipost
Hosted MCP server for AI agents to post to LinkedIn profiles and X and upload to YouTube.
Related MCP Servers
- AlicenseAqualityCmaintenanceCross-publishes a single markdown draft to 5 CMS platforms (dev.to, Hashnode, Ghost, WordPress, Medium) and 4 social networks (Bluesky, Mastodon, LinkedIn, X) from any MCP client, with built-in SEO scoring, schema.org JSON-LD, and canonical URL handling so cross-posts compound search rankings instead of competing.2931 npm5MIT
- AlicenseAqualityBmaintenanceStandalone Substack CLI + 26-tool MCP server. Your IDE drafts the replies. Zero AI API keys.261MIT
- AlicenseAqualityBmaintenanceAn MCP server for WriteFreely that enables AI agents to publish and manage content on WriteFreely instances (including self-hosted instances and Write.as).103MIT
- AlicenseNot gradedqualityAmaintenanceA local-first MCP server for social publishing on X (Twitter), enabling draft management, scheduling, and posting via CLI or AI agents.41 npm5MIT