Skip to content

03 — Write a Blog Draft

This is the bread-and-butter flow — ask Claude to write a blog post, have the worker stash it as a draft on a WordPress site, then review it in wp-admin before publishing manually.

You

Write a 300-word blog post about “Top 5 free SEO plugins for WordPress in 2026” and save it as a draft on viper-template. Use tags seo, plugins.

Claude
create_draft

Done — draft created on viper-template:

  • Post ID: 47
  • Status: draft
  • Edit in wp-admin: https://viper-template.on-forge.com/wp-admin/post.php?post=47&action=edit

The post is not live yet. Open the link above to review and click Publish when you’re happy. Tags seo and plugins were created as new terms because they didn’t exist on the site.

The “drafts only” hard rule means even if you ask Claude to publish immediately, it will create a draft and stop:

You

Actually publish it now.

Claude

I can’t publish posts directly — the SEO Navigator MCP server intentionally has no publish_post tool. You’ll need to open the post in wp-admin and click Publish there. I can schedule it for a future time though — just tell me when.

Every create_draft call lands in the audit log with the user, the site, and the result. This is the source of truth for “did Claude actually do what it said”.

Audit log showing create_draft entries — status ok, viper-template

If the row shows status error or denied, check the error column on the right — typical causes are an expired WP app password (401 Unauthorized from WordPress), the site not having the configured app password yet, or the token lacking create_draft in its allowed_tools.

Prove the happy path for create_draft: Claude composes content, sends it to a WordPress site, the post appears as Draft in wp-admin, and no live URL exists yet.

  • Scenarios 01–02 done; connector working; at least one WordPress site is reachable.
  • The user has a WP admin login for that site (only needed to verify the result, not for the MCP call).
  • Claude Desktop on a new conversation.
  • A second tab open on <wp-site>/wp-admin/edit.php?post_status=draft&post_type=post, showing the current list of drafts (note the count for comparison).
  1. Action: In the composer, type:

    Write a 300-word blog post titled “Five SEO myths that waste your budget” for my site acme-blog. Save it as a draft. Use a friendly, direct tone.

    Press Enter. Expected: Claude streams the draft body, then calls create_draft with site_id="acme-blog", title, content, status="draft". The tool-use card resolves green. Recorder: capture the tool-use card opening to show the arguments — status: "draft" is the load-bearing field.

  2. Action: Wait for Claude’s final reply. Expected: Claude replies with the new post’s id, slug, and the edit URL …/wp-admin/post.php?post=<id>&action=edit. Recorder: hover the edit URL — Claude renders it as a link.

  3. Action: Click the edit URL (or copy it into the WP admin tab). Expected: WordPress opens the post editor with the title and body filled in, and the status pill at the top reads Draft. Recorder: hold on the Draft status pill for two seconds — that is the proof of the no-publish guardrail.

  4. Action: Return to the drafts list (edit.php?post_status=draft). Expected: The new post is the first row; the Date column reads Last Modified: just now and the row has no published URL. Recorder: highlight the new row with a callout box.

  • Admin dashboard Audit log has a fresh row: tools/call create_draft with status ok, target site acme-blog, and the new post id in the response payload.
  • GET /wp-json/wp/v2/posts/<id>?context=edit (via curl with the app password) returns status: "draft".
  • In WP admin, move the draft to Trash. (The MCP cannot do this — the guardrail forbids DELETE.)
  • Optionally empty the trash from WP admin.
  • status: "publish" injected by the user prompt: e.g. “…and publish it now.” — Claude either refuses or downgrades to draft. The adapter returns ForbiddenOperationError if forced; show the red tool card and Claude’s apology message.
  • Site id wrong: e.g. acme-blogg. The tool returns E_SITE_NOT_FOUND. Show Claude calling list_sites to recover.
  • Empty content: WP REST returns 400. Show the error bubbling up as a red card.