---
title: "How I Added AI-Powered Content Creation to My WordPress Blog with MCP"
author: "Lax Mariappan"
date: "2025-12-24"
categories: ["AI", "WordPress"]
excerpt: "I just added a pretty cool feature to my blog: the ability to create posts by chatting with Claude AI. Here’s how I did it, explained in plain English—including all the mistakes I made along the way. 📦 Full source code available: github.com/laxmariappan/claude2blog What is MCP? MCP (Model Context Protocol) is a way for AI […]"
canonical_url: "https://laxmariappan.com/how-i-added-ai-powered-content-creation-to-my-wordpress-blog-with-mcp/"
---

# How I Added AI-Powered Content Creation to My WordPress Blog with MCP

I just added a pretty cool feature to my blog: the ability to create posts by chatting with Claude AI. Here’s how I did it, explained in plain English—including all the mistakes I made along the way.

📦 Full source code available: github.com/laxmariappan/claude2blog

What is MCP?

MCP (Model Context Protocol) is a way for AI assistants like Claude to interact with applications. Think of it as a bridge that lets Claude actually do things instead of just suggesting them.

Instead of Claude telling me “here’s the content, now copy-paste it to WordPress,” Claude can now publish directly to my blog.

What You Need

WordPress 6.9 or newer – This includes the new Abilities API

MCP Adapter plugin – Available from the WordPress plugin directory

Node.js – To run the MCP bridge locally

Basic PHP knowledge – Just enough to create a simple WordPress plugin

The Big Picture

WordPress 6.9 introduced something called the “Abilities API.” It lets you register actions that your site can perform, like “create a post” or “get site info.”

The MCP Adapter plugin automatically discovers and exposes registered WordPress abilities to AI assistants through the MCP protocol.

The Journey: What Actually Happened

Attempt 1: Following the Docs (Sort Of)

I started by registering abilities using wp_register_ability(). My initial code looked like this:

add_action('abilities_init', 'claude_register_publish_ability');

function claude_register_publish_ability() {
    wp_register_ability('claude/publish-post', [
        'label' => 'Publish Blog Post',
        'description' => 'Publish a blog post',
        'callback' => 'claude_publish_post_handler',
        'capability' => 'publish_posts',
        'input_schema' => [
            'title' => ['type' => 'string', 'required' => true],
            'content' => ['type' => 'string', 'required' => true],
            // ...
        ],
    ]);
}
Code language: PHP (php)

Result: Abilities weren’t showing up in MCP discovery. Zero. Nada. Nothing.

Problem 1: Wrong Hook Name

The correct hook is wp_abilities_api_init, not abilities_init. Easy mistake to make when you’re working with new APIs.

// WRONG
add_action('abilities_init', 'claude_register_publish_ability');

// CORRECT
add_action('wp_abilities_api_init', 'claude_register_publish_ability');
Code language: JavaScript (javascript)

Problem 2: Wrong Namespace

I initially used claude/publish-post as the ability name. Bad idea—”Claude” is Anthropic’s trademark. Changed all abilities to use claude2blog/ namespace instead:

// BEFORE: claude/publish-post
// AFTER: claude2blog/publish-post
Code language: JSON / JSON with Comments (json)

Problem 3: Wrong Parameter Names

The official Abilities API documentation clearly states you need execute_callback and permission_callback, not just callback:

// WRONG
wp_register_ability('claude2blog/publish-post', [
    'callback' => 'claude_publish_post_handler',
    'capability' => 'publish_posts',
]);

// CORRECT
wp_register_ability('claude2blog/publish-post', [
    'execute_callback' => 'claude_publish_post_handler',
    'permission_callback' => 'claude_publish_post_permission',
]);
Code language: PHP (php)

The permission callback is a separate function that checks capabilities:

function claude_publish_post_permission($params) {
    return current_user_can('publish_posts');
}
Code language: PHP (php)

Problem 4: The Smoking Gun – Missing Category Parameter

This was the hardest bug to find. Even with correct hook names and parameter names, abilities still weren’t discoverable. They would register, but wp_register_ability() silently returned null instead of a WP_Ability object.

After creating a minimal test plugin with extensive debug logging, I discovered the root cause:

error_log('[TEST-ABILITY] Registration returned NULL!');
error_log('[TEST-ABILITY] Available categories: site, user, mcp-adapter');
Code language: JavaScript (javascript)

I was missing the required category parameter! Looking at the WordPress core source (wp-includes/abilities-api/class-wp-ability.php lines 274-278), I found:

if ( empty( $args['category'] ) || ! is_string( $args['category'] ) ) {
    throw new InvalidArgumentException(
        __( 'The ability properties must contain a `category` string.' )
    )
}
Code language: PHP (php)

The category parameter is REQUIRED and must reference a registered category. Available categories are:

site – Site-level operations

user – User-level operations

mcp-adapter – MCP-specific abilities

Adding this one parameter fixed everything:

wp_register_ability('claude2blog/publish-post', [
    'label' => __('Publish Blog Post', 'claude-publisher'),
    'description' => __('Publish a blog post with title, content, categories, tags, and more', 'claude-publisher'),
    'category' => 'mcp-adapter',  // THIS WAS THE MISSING PIECE!
    'input_schema' => [/* ... */],
    'execute_callback' => 'claude_publish_post_handler',
    'permission_callback' => 'claude_publish_post_permission',
    'meta' => [
        'mcp' => ['public' => true],
        'show_in_rest' => true,
    ],
]);
Code language: PHP (php)

Problem 5: MCP Metadata Requirements

For abilities to be discoverable via MCP, they need specific metadata. I discovered this by reading the MCP Adapter source code (wp-content/plugins/mcp-adapter/includes/Abilities/McpAbilityHelperTrait.php):

$is_public_mcp = $meta['mcp']['public'] ?? false;

if ( ! ( $meta['mcp']['public'] ?? false ) ) {
    continue;  // Skip if not publicly exposed
}
Code language: PHP (php)

So abilities need 'mcp' => ['public' => true] in their meta array to be exposed via MCP. They also need 'show_in_rest' => true to appear in the REST API.

The Working Code

After all the debugging, here’s what actually works:

add_action('wp_abilities_api_init', 'claude_register_publish_ability');

function claude_register_publish_ability() {
    // Check if Abilities API is available
    if (!function_exists('wp_register_ability')) {
        return;
    }

    // Register the publish post ability
    wp_register_ability('claude2blog/publish-post', [
        'label' => __('Publish Blog Post', 'claude-publisher'),
        'description' => __('Publish a blog post with title, content, categories, tags, and more', 'claude-publisher'),
        'category' => 'mcp-adapter',
        'input_schema' => [
            'title' => [
                'type' => 'string',
                'required' => true,
                'description' => 'Post title',
            ],
            'content' => [
                'type' => 'string',
                'required' => true,
                'description' => 'Post content (HTML allowed)',
            ],
            'status' => [
                'type' => 'string',
                'enum' => ['draft', 'publish', 'pending', 'private'],
                'default' => 'draft',
                'description' => 'Post status',
            ],
            // ... more fields
        ],
        'execute_callback' => 'claude_publish_post_handler',
        'permission_callback' => 'claude_publish_post_permission',
        'meta' => [
            'mcp' => ['public' => true],
            'show_in_rest' => true,
        ],
    ]);
}

function claude_publish_post_permission($params) {
    return current_user_can('publish_posts');
}

function claude_publish_post_handler($params) {
    // Validate required parameters
    if (empty($params['title']) || empty($params['content'])) {
        return new WP_Error(
            'missing_required_fields',
            __('Title and content are required', 'claude-publisher')
        );
    }
    
    // Prepare post data
    $post_data = [
        'post_title' => sanitize_text_field($params['title']),
        'post_content' => wp_kses_post($params['content']),
        'post_status' => isset($params['status']) ? sanitize_text_field($params['status']) : 'draft',
        'post_author' => get_current_user_id(),
    ];
    
    // Handle categories
    if (!empty($params['categories']) && is_array($params['categories'])) {
        $post_data['post_category'] = array_map('absint', $params['categories']);
    }
    
    // Insert the post
    $post_id = wp_insert_post($post_data, true);
    
    if (is_wp_error($post_id)) {
        return $post_id;
    }
    
    // Handle tags
    if (!empty($params['tags']) && is_array($params['tags'])) {
        wp_set_post_tags($post_id, array_map('absint', $params['tags']));
    }
    
    return [
        'success' => true,
        'post_id' => $post_id,
        'post_url' => get_permalink($post_id),
        'edit_url' => get_edit_post_link($post_id, 'raw'),
        'message' => sprintf(
            __('Post "%s" published successfully', 'claude-publisher'),
            get_the_title($post_id)
        ),
    ];
}
Code language: PHP (php)

The MCP Bridge (The Other Tricky Part)

WordPress MCP Adapter requires session management to work properly. This was another debugging adventure.

The Session Challenge

WordPress MCP Adapter uses a custom session management system (not part of the standard MCP protocol). Here’s how it works:

When Claude first connects, it sends an initialize request

WordPress creates a session and returns a Mcp-Session-Id in the response header

The bridge must capture this session ID and reuse it for ALL subsequent requests

Sessions expire after 24 hours of inactivity

If you generate a new session ID for every request (which I initially did), you’ll get “Invalid or expired session” errors.

The Solution: Session Persistence

Here’s the key code in the Node.js bridge that makes it work:

// Session management
let currentSessionId = null;

function makeRequest(method, body, timeout = 5000) {
  return new Promise((resolve, reject) => {
    const headers = {
      'Content-Type': 'application/json',
      'Authorization': authHeader
    };

    // Only add Mcp-Session-Id if we have one (not needed for initialize)
    if (currentSessionId) {
      headers['Mcp-Session-Id'] = currentSessionId;
    }

    const req = httpModule.request(options, (res) => {
      // Capture session ID from response headers
      const responseSessionId = res.headers['mcp-session-id'];
      if (responseSessionId) {
        currentSessionId = responseSessionId;  // Store it!
      }
      // ... handle response ...
    });
  });
}
Code language: JavaScript (javascript)

How It Actually Works

The complete flow looks like this:

┌────────────────┐
│  Claude Code   │
│ Claude Desktop │
└────────┬───────┘
         │ STDIO (JSON-RPC 2.0, MCP 2025-06-18)
         ▼
┌────────────────┐
│  MCP Bridge    │  ← Session ID persistence
│  (Node.js)     │  ← Protocol translation
└────────┬───────┘
         │ HTTP + Auth + Mcp-Session-Id header
         │ (MCP 2024-11-05)
         ▼
┌────────────────────┐
│  WordPress MCP     │
│  Endpoint          │  ← Session validation
└────────┬───────────┘
         │ Abilities API
         ▼
┌────────────────────┐
│  Your Plugin       │
│  wp_register_      │
│  ability()         │
└────────┬───────────┘
         │ WordPress Core
         ▼
┌────────────────────┐
│  wp_insert_post()  │
└────────────────────┘

Key Lessons Learned

Read the error logs carefully – The “Registration returned NULL” message was the key clue

The category parameter is REQUIRED – Not mentioned prominently in quick-start guides but enforced in core

MCP metadata matters – You need 'mcp' => ['public' => true] for MCP exposure

Session management is crucial – You must capture and reuse the session ID

Use correct parameter names – execute_callback and permission_callback, not just callback

Hook names matter – It’s wp_abilities_api_init, not abilities_init

Namespaces are important – Don’t use trademarked names like “claude”

Full Code Available

I’ve open-sourced the complete working implementation:

Repository: github.com/laxmariappan/claude2blog

Includes:

WordPress plugin with correct ability registration (v1.7.0)

Node.js MCP bridge with session persistence

Complete setup guide

Example configuration files

Troubleshooting documentation based on real debugging experience

Try It Yourself

Check out the repository for step-by-step setup instructions. The debugging took a couple of days (mostly figuring out the missing category parameter and session management), but now it works flawlessly.

Being able to just chat with Claude and have posts appear on my blog feels like magic. No more copy-pasting. No more context switching. Just conversation.

This post itself was updated via MCP using the claude2blog/update-post ability!