Fix: MCP Server Not Detecting WordPress Abilities

by Alex Johnson 50 views

Are you building a WordPress plugin and trying to expose custom functionalities through the MCP (Message, Command, and Process) server, only to be met with a frustrating "Tool not found" error? You're not alone! Many developers run into this issue when registering their WordPress abilities, especially when using the default MCP server. This article dives deep into why this happens and, more importantly, how to fix it, ensuring your custom abilities are accessible and ready to be called.

Understanding the MCP Server and WordPress Abilities

Before we troubleshoot, let's quickly recap what MCP and WordPress abilities are all about. The MCP server in WordPress, often facilitated by plugins like mcp-adapter, acts as a bridge. It allows external tools or command-line interfaces to interact with your WordPress site by calling specific functions, often referred to as "abilities." These abilities are essentially predefined actions that your WordPress site can perform. Think of them as a way to automate tasks, retrieve data, or even trigger complex processes within your WordPress environment using a structured, JSON-RPC-based communication protocol. This is particularly powerful for developers who want to integrate their WordPress site with other applications or build custom command-line tools for managing their site.

Now, WordPress abilities are the specific functions you register within your WordPress site to be exposed via the MCP server. These are not just random PHP functions; they are carefully defined with input schemas, output schemas, and callbacks that dictate how they should be executed and what data they expect or return. The wp_register_ability and wp_register_ability_category functions are your go-to tools for this. You define a unique name for your ability (like fluentaccount/get-posts), provide a user-friendly label and description, specify the category it belongs to, and define the structure of the data it accepts (input schema) and returns (output schema). Crucially, you also provide a execute_callback function – the actual PHP code that performs the desired action – and a permission_callback to ensure only authorized users or processes can trigger the ability.

The goal is to make your plugin's functionalities accessible programmatically, allowing for seamless automation and integration. However, the communication between the MCP server and the WordPress ability registration system needs to be perfectly timed. If the server starts listening for abilities before your custom abilities have been registered, it simply won't find them, leading to the common "Tool not found" error you might be encountering. This timing issue is at the heart of the problem we'll be solving.

The Root Cause: Timing is Everything

The core of the problem lies in the execution order of WordPress actions and when the MCP adapter decides to initialize itself. When you register your custom abilities using add_action with hooks like wp_abilities_api_categories_init and wp_abilities_api_init, these registrations happen at specific points during the WordPress loading process. However, the WP\[MCP](https://mcpadapter.com/docs/en/latest/introduction/quick-start/)\Core\McpAdapter::instance() call, which effectively starts the MCP server listening for requests, also needs to happen at the right moment. If McpAdapter::instance() is called too early in the WordPress lifecycle, it might try to serve requests or discover available tools before your wp_register_ability calls have had a chance to execute.

Imagine the MCP server as a waiter in a restaurant. Your abilities are the dishes you're preparing in the kitchen. If the waiter (MCP server) starts taking orders (listening for requests) before the chefs (your plugin's registration code) have even finished preparing the menu (registering abilities), the waiter won't know about your special dishes. They'll look at the incomplete menu and report that your desired dish isn't available, even though you're about to serve it.

In the provided code example, the abilities are registered using actions that are typically fired relatively early in the WordPress load. However, the MCP adapter's initialization needs to be carefully placed to ensure it only starts after all custom abilities have been registered. The documentation often suggests that the default server should automatically expose all abilities, which is true, but only if those abilities are already registered when the server initializes. The plugins_loaded hook is generally a good candidate for initializing core plugin functionality, but the priority at which it's hooked matters significantly. If the MCP adapter is initialized on a plugins_loaded hook with a default or early priority, it might still run before your abilities are fully registered.

This timing mismatch is a common pitfall, especially in complex WordPress environments where multiple plugins and themes might hook into various actions. The mcp-adapter relies on the WordPress API to discover registered abilities. If the API hasn't been populated with your custom abilities by the time the adapter queries it, the server will naturally report that the tool (your ability) doesn't exist. The CLI test you performed, using wp mcp-adapter serve --user=admin --server=mcp-adapter-default-server, is a perfect demonstration of this. It sends a request to the default server, which then fails to locate fluentaccount/get-posts because, at that moment, the server's internal registry of abilities is empty or incomplete.

The Solution: Hooking into the Right Actions with Precision

To resolve the "Tool not found" error, we need to ensure that your WordPress abilities are registered before the MCP adapter initializes and starts listening for requests. The key lies in using WordPress action hooks with appropriate priorities. The provided solution suggests a clever way to achieve this by adjusting the hook priorities.

Let's break down the recommended approach:

  1. Register Abilities Early: You're already using wp_abilities_api_categories_init and wp_abilities_api_init. These are the correct hooks for defining your ability categories and the abilities themselves. The crucial part here is when these hooks fire in relation to the MCP adapter's initialization. If your plugin loads at a standard priority, these hooks will fire at their default priorities.

  2. Initialize MCP Adapter with a Low Priority: The proposed fix involves hooking the initialization of the MCP adapter (WP\[MCP](https://mcpadapter.com/docs/en/latest/introduction/quick-start/)\Core\McpAdapter::instance();) to the plugins_loaded action, but with a specific, low priority. By default, plugins_loaded might fire at priority 10. However, by setting a higher number for the priority (e.g., 5 as suggested in the workaround, which is actually lower in execution order if we consider that lower numbers execute first), you're telling WordPress: "Wait a bit longer before running this specific function." More accurately, if we want it to run after other plugins_loaded actions, we should give it a higher priority number. For instance, if most other plugins_loaded actions run at 10, running McpAdapter::instance() at 99 would ensure it runs much later. The provided example uses 5 which might be counter-intuitive if you're used to thinking lower numbers run first. Let's clarify: WordPress fires actions in ascending order of priority. So, a priority of 5 will execute before actions with priority 10 or higher. If your ability registration hooks are also using default priorities (like 10), then hooking McpAdapter::instance() to plugins_loaded with priority 5 would indeed cause it to run before your abilities are registered. The goal is to make McpAdapter::instance() run after your wp_register_ability calls. Therefore, if your ability registrations are hooked to actions like wp_abilities_api_init (which has a default priority), you should initialize McpAdapter with a higher priority number on plugins_loaded (e.g., 20 or 30) or hook it to an even later action like init (which has a default priority of 10) or wp_loaded (default priority 1000) if necessary, but ensuring it's after your registrations.

Correction to the workaround: The example uses add_action('plugins_loaded', function() { ... }, 5);. If wp_abilities_api_init fires at a later priority (e.g., 10), then priority 5 for plugins_loaded would execute before wp_abilities_api_init. To ensure MCP initialization happens after ability registration, you would need to give McpAdapter::instance() a higher priority number on plugins_loaded than the priority of your wp_abilities_api_init hook. A common strategy is to hook MCP initialization to wp_loaded which fires very late, or ensure your plugins_loaded priority is significantly higher than any other plugins_loaded action that might register abilities.

A more robust approach:

// Register abilities with default or appropriate priorities
add_action('wp_abilities_api_categories_init', function() {
    // ... register category ...
});

add_action('wp_abilities_api_init', function () {
    // ... register ability ...
});

// Initialize MCP Adapter VERY LATE in the loading process
add_action('wp_loaded', function() {
    if (class_exists(WP\[MCP](https://mcpadapter.com/docs/en/latest/introduction/quick-start/)\Core\McpAdapter::class)) {
        // Use MCPAdapter::instance() only if it exists
        WP\[MCP](https://mcpadapter.com/docs/en/latest/introduction/quick-start/)\Core\McpAdapter::instance();
    }
}, 1000); // High priority to ensure it runs after most other things

The wp_loaded hook is guaranteed to run after WordPress has fully loaded, including all plugins and the theme. By hooking the MCP adapter initialization here, you significantly increase the chances that all abilities registered via earlier hooks will have already been processed. The priority 1000 is very high, ensuring it executes near the end of the wp_loaded sequence.

Always include a class_exists() check to prevent errors if the mcp-adapter plugin isn't active or available for some reason. This defensive programming ensures your site remains stable.

Implementing the Fix in Your Plugin

Let's take the provided PHP code and refactor it using the wp_loaded hook for initializing the MCP adapter.

<?php
/**
 * Plugin Name: Fluent Account MCP Adapter
 * Description: Registers Fluent Account abilities for MCP Server.
 * Version: 1.0
 * Author: Your Name
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

// Ensure the MCP Adapter class exists before attempting to use it.
if ( class_exists( '\WP\MCP\Core\McpAdapter' ) ) {

    // Register the ability category
    add_action('wp_abilities_api_categories_init', function() {
        wp_register_ability_category('fluentaccount', [
            'label' => 'Fluent Account',
            'description' => 'Abilities related to the FluentAccount plugin',
        ]);
    });

    // Register the ability
    add_action('wp_abilities_api_init', function () {
        wp_register_ability('fluentaccount/get-posts', [
            'label' => 'Get Posts',
            'description' => 'Retrieve WordPress posts with optional filtering',
            'category' => 'fluentaccount', // must match slug
            'input_schema' => [
                'type' => 'object',
                'properties' => [
                    'numberposts' => [
                        'type' => 'integer',
                        'description' => 'Number of posts to retrieve',
                        'default' => 5,
                        'minimum' => 1,
                        'maximum' => 100,
                    ],
                    'post_status' => [
                        'type' => 'string',
                        'description' => 'Post status to filter by',
                        'enum' => ['publish', 'draft', 'private'],
                        'default' => 'publish',
                    ],
                ],
                'required' => [],
            ],
            'output_schema' => [
                'type' => 'array',
                'items' => [
                    'type' => 'object',
                    'properties' => [
                        'ID'           => ['type' => 'integer'],
                        'post_title'   => ['type' => 'string'],
                        'post_content' => ['type' => 'string'],
                        'post_date'    => ['type' => 'string'],
                        'post_author'  => ['type' => 'string'],
                    ],
                ],
            ],
            'execute_callback' => function ($input) {
                $args = [
                    'numberposts' => $input['numberposts'] ?? 5,
                    'post_status' => $input['post_status'] ?? 'publish',
                ];
                // Ensure we're getting actual posts, not just IDs or summaries
                $posts = get_posts($args);
                // Format output to match schema, especially if get_posts returns full WP_Post objects
                $formatted_posts = [];
                foreach ($posts as $post) {
                    $formatted_posts[] = [
                        'ID' => $post->ID,
                        'post_title' => $post->post_title,
                        'post_content' => $post->post_content, // Be mindful of content length in production
                        'post_date' => $post->post_date,
                        'post_author' => get_the_author_meta('display_name', $post->post_author),
                    ];
                }
                return $formatted_posts;
            },
            'permission_callback' => function () {
                // A more robust permission check might be needed depending on your use case.
                // For example, checking for a specific capability.
                return current_user_can('read');
            },
        ]);
    });

    // Initialize MCP Adapter LATE in the WordPress loading process
    // Using 'wp_loaded' hook with a high priority ensures it runs after most other initializations.
    add_action('wp_loaded', function() {
        // Re-check class_exists for safety, although it's checked above.
        if (class_exists( '\WP\MCP\Core\McpAdapter' )) {
            \WP\MCP\Core\McpAdapter::instance();
            // You might want to log or confirm initialization here if needed for debugging.
            // error_log('MCP Adapter initialized successfully.');
        }
    }, 1000); // Priority 1000 ensures it runs very late.

}

By changing the hook for WP\[MCP](https://mcpadapter.com/docs/en/latest/introduction/quick-start/)\Core\McpAdapter::instance() to wp_loaded with a high priority like 1000, you ensure that WordPress has completed most of its loading procedures, including the registration of your abilities via the wp_abilities_api_init hook. This should allow the default MCP server to correctly discover and list your fluentaccount/get-posts ability.

After implementing this change, try your CLI test again:

echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"fluentaccount/get-posts","arguments":{"numberposts":3,"post_status":"publish"}}}' \
| wp mcp-adapter serve --user=admin --server=mcp-adapter-default-server

You should now see a successful JSON-RPC response instead of the "Tool not found" error. Remember to test thoroughly and adjust priorities if you encounter unexpected behavior, though wp_loaded is generally a safe bet for late initialization.

Advanced Considerations and Debugging

While adjusting hook priorities is the most common solution, several other factors can influence whether your abilities are detected. Debugging these issues requires a systematic approach.

Verify Plugin Activation and Dependencies

First, ensure that both your plugin (containing the ability registration code) and the mcp-adapter plugin are activated in your WordPress installation. The class_exists('\WP\MCP\Core\McpAdapter') check is crucial here. If the adapter plugin isn't active, its class won't exist, and your code gracefully won't attempt to initialize it, preventing fatal errors. However, this also means no MCP server will be running to detect your abilities.

Check WordPress Hooks and Priorities

If you're using a complex plugin or theme that heavily manipulates WordPress hooks, it's possible that another plugin is unintentionally interfering with the timing. Use a plugin like Query Monitor (which is excellent for debugging WordPress) to inspect the list of active hooks and their priorities. Look for wp_abilities_api_init and wp_loaded. See what other plugins are hooking into these or related actions and at what priorities. This can help you identify if another plugin is initializing MCP adapter very early or delaying your ability registrations.

Inspect MCP Adapter Logs

The mcp-adapter itself might have logging capabilities. Check your server's error logs or any specific logs generated by the adapter for more detailed information about its initialization process and any errors it encounters. The [MCP STDIO Bridge] MCP STDIO Bridge started for server: mcp-adapter-default-server and [MCP STDIO Bridge] MCP STDIO Bridge stopped messages in your output are good indicators that the bridge is attempting to function, but the subsequent error reveals the core problem.

Use error_log() for Debugging

Strategically place error_log() statements within your ability registration callbacks and within the MCP adapter initialization logic. For instance:

add_action('wp_abilities_api_init', function () {
    error_log('Registering fluentaccount/get-posts ability...');
    wp_register_ability('fluentaccount/get-posts', [
        // ... ability definition ...
    ]);
    error_log('fluentaccount/get-posts ability registered.');
});

add_action('wp_loaded', function() {
    error_log('Attempting to initialize MCP Adapter...');
    if (class_exists( '\WP\MCP\Core\McpAdapter' )) {
        \WP\MCP\Core\McpAdapter::instance();
        error_log('MCP Adapter initialized.');
    } else {
        error_log('MCP Adapter class not found.');
    }
}, 1000);

Check your debug.log file (usually located in wp-content/) for these messages. This helps you trace the execution flow and confirm whether your registration code is running and whether the adapter is being initialized.

Simplify for Testing

If you're still facing issues, try simplifying your setup. Temporarily deactivate all other plugins except mcp-adapter and your custom plugin. Use a default WordPress theme. This isolates the problem to the interaction between your plugin and the MCP adapter, removing potential conflicts.

By systematically working through these debugging steps, you can pinpoint the exact cause of the timing issue and ensure your WordPress abilities are reliably exposed via the MCP server. Remember, the key is often a matter of patience and understanding the WordPress execution order.

Conclusion

Encountering the "Tool not found" error when using the default MCP server with custom WordPress abilities can be a head-scratcher, but it almost always boils down to a timing issue. The MCP server needs to be initialized after your abilities have been successfully registered with WordPress. By carefully managing your action hook priorities, particularly by initializing the WP\MCP\Core\McpAdapter on a later hook like wp_loaded with a high priority, you can ensure this synchronization happens correctly.

This approach guarantees that when the MCP server scans for available tools, your custom abilities are present and accounted for, ready to be called via CLI or other compatible tools. Remember to always include checks for class existence and consider using error_log or debugging plugins for more complex scenarios. With these adjustments, you can confidently leverage the power of the MCP server to extend your WordPress site's functionality programmatically.

For more in-depth information on WordPress hooks and actions, consult the official WordPress Developer Resources at developer.wordpress.org. For details specific to MCP Adapter, refer to its official documentation available at MCP Adapter Documentation.