Skip to content

WordPress blog posts

The blog-post workflow is the simplest thing this worker does, and a good first task to confirm everything is wired correctly. You stay in chat the whole time; the worker writes drafts via WP REST that you publish manually when ready.

ToolWhat it does
create_draftMake a new draft post
update_draftPatch fields on an existing draft
schedule_draftFlip a draft to status=future at a chosen ISO8601 time
unscheduleRoll a scheduled post back to draft
get_postInspect any single post
list_draftsList drafts + scheduled posts
upload_mediaPush an image and get the URL back
list_taxonomies / create_taxonomyTags + categories

All of them are first-party tools — they don’t need the wp-mcp-adapter plugin. You only need:

  • A WordPress site added at /admin/sites with platform wordpress.
  • An Application Password set (the admin form stores it AES-GCM in D1).
  • A token (OAuth Custom Connector or bearer) that includes the site.

In a chat where the SEO Navigator connector is enabled:

Write a 400-word draft on site blog-acme titled “How ceramic coating works”, aimed at car owners who’ve never heard the term. Friendly tone, no bullet lists, end with a single CTA to call us.

Claude will (typically):

  1. Call list_sites if it doesn’t already know blog-acme exists.
  2. Compose the body.
  3. Call create_draft with site_id, title, content_md or content_html.

The result includes the WP post ID, the admin edit URL, and the status — which will be draft, every time. The post is invisible to the public until you click Publish in wp-admin.

Bump the intro of post 1234 on blog-acme to mention the 9H hardness rating. Keep everything else.

Claude calls get_post to fetch the current body, edits, then calls update_draft with post_id: "1234" and a fresh content_md. If you don’t say “keep everything else”, expect Claude to rewrite the whole post — be explicit.

update_draft refuses to touch a post that is already published — it returns E_FORBIDDEN_OP at the adapter layer. That’s deliberate: this worker only deals with drafts.

Schedule post 1234 on blog-acme for next Friday at 9am Boston time.

Claude resolves the time zone and calls schedule_draft with an ISO8601 scheduled_at. The worker requires the time to be at least 60 seconds in the future — anything sooner is rejected with E_FORBIDDEN_OP, to block “schedule now” tricks.

To pull a scheduled post back:

Move post 1234 back to draft.

Claude calls unschedule → status returns to draft. The post is no longer on the WP scheduler queue.

WordPress REST /wp/v2/posts rejects tag/category arrays unless they’re integer term IDs — not names. The worker handles this for you in two places:

  • create_draft / update_draft accept either names or numeric IDs. Names are matched case-insensitively against existing terms; missing ones are created on the fly via create_taxonomy.
  • deploy_md_to_site (the markdown-sync path, see Content + Design teams) uses the same auto-resolution under the hood.

Tag post 1234 with “ceramic coating”, “paint protection”, “Boston”.

Claude calls update_draft with tags: ["ceramic coating", "paint protection", "Boston"]. Behind the scenes the worker lists existing tags, finds matches, creates “Boston” if it doesn’t exist, then patches the post with the resolved integer IDs.

If you want to see the current term list first:

List tags on blog-acme.

Claude calls list_taxonomies with type: "tags".

Two ways to attach a hero image:

1. Upload, then use the URL.

Upload the image at https://images.unsplash.com/photo-1234 to blog-acme as the featured image for post 1234.

Claude calls upload_media with source_url set, then update_draft with featured_image_url. The worker fetches the URL server-side and posts the bytes to WP’s /media endpoint — it doesn’t hotlink.

2. Inline (Claude has the bytes already).

upload_media also accepts base64 if Claude generated or screenshot-grabbed the image. Same shape, just base64 + mime instead of source_url.

Show me drafts and scheduled posts on blog-acme.

Claude calls list_drafts (default include_scheduled: true). The worker asks WP for both status=draft and status=future, merges, and returns post id + title + status + scheduled date.

If you only want drafts:

Show drafts only (not scheduled) on blog-acme.

Claude passes include_scheduled: false.

list_sites filtered by platform lets you ask Claude things like:

Across all my WordPress sites, show me drafts I haven’t published this week.

Claude calls list_sites { platform: "wordpress" }, iterates list_drafts per site, and compiles the result. There’s no built-in cross-site index — the worker iterates serially. With ten sites and three drafts each, expect ~3 seconds end-to-end.

WP posts ship with an author ID (the integer of a user account). The worker:

  • Defaults to the Application Password user (the one in wp_username).
  • Honors a author argument on create_draft if you pass a numeric WP user ID — useful for ghost-writing flows (“write as user 7”).
  • Won’t let you set the author to someone the App Password doesn’t have permission to act as (WP returns 401, surfaced as E_UPSTREAM).
SymptomWhat’s happeningFix
E_FORBIDDEN_OP — status=publish refusedSomething asked for an immediate publishThis worker only drafts. Use schedule_draft with a future time instead.
E_FORBIDDEN_OP — scheduled_at too soonTime is within 60s of nowPick a time at least a minute out.
E_UPSTREAM — WP 400 invalid term idTag/category passed as wrong typePass strings (names) — the worker resolves them. Numeric IDs also accepted.
E_UPSTREAM — WP 401App Password wrong / user lacks capabilitiesRe-issue the Application Password in wp-admin.
E_SCOPE_DENIEDToken doesn’t include this site/admin/tokens → edit → tick the site.
Draft shows up but no featured imageupload_media failed silentlyCheck /admin/audit for the matching call; common cause is image > 10 MB.