Skip to content

Detect AI Tool Calls in Your Own Code ​

Easy MCP AI gives site code a stable way to tell when it is running because an AI client called a tool. A publishing guard, a must-use plugin or a theme can use it to block an AI-made change, mark posts an AI edited, or log every call. This guide covers the two helper functions, the EASY_MCP_AI_MCP_REQUEST constant, their filters, and the actions that fire before and after every tool call.

Everything on this page is a public contract: the names, arguments and answers do not change without a deprecation notice. The plugin's internal classes are not part of it and may change in any release, so build on these functions and hooks rather than on internal methods.

In this article

Which Signal to Use ​

QuestionUseTrue when
Is this request being served by the MCP endpoint?easy_mcp_ai_is_mcp_request() or the EASY_MCP_AI_MCP_REQUEST constantWordPress has routed the request to the MCP endpoint
Is a tool running right now?easy_mcp_ai_is_tool_call()A tool call is executing
Run something just before or just after every tool callThe easy_mcp_ai_before_tool_call and easy_mcp_ai_after_tool_call actionsEvery tool call that reaches execution
Run something after a tool changed the siteThe easy_mcp_ai_tool_mutated actionA tool that writes has succeeded

The two helpers answer different questions, so they can disagree:

Momenteasy_mcp_ai_is_mcp_request()easy_mcp_ai_is_tool_call()
plugins_loaded, init or rest_api_init on an MCP requestfalse (the request has not been routed yet)false
The client connecting, listing tools or authenticatingtruefalse
Inside a tool, for example save_post fired by wp_update_posttruetrue
easy_mcp_ai_after_tool_call for the client's own calltruefalse
A tool call run in the background by WP-Cron (a long task)falsetrue
A visitor's page view, wp-admin or WP-CLIfalsefalse

Always guard with function_exists(), so your code keeps working while the plugin is deactivated.

Check Whether a Tool Is Running ​

easy_mcp_ai_is_tool_call() returns true while a tool call is executing. That includes the tools a composite tool such as wp_cli_run or wp_history_apply runs inside its own call, and the read tools wp_cli_run runs to plan a command.

php
add_action( 'save_post', function ( $post_id ) {
    if ( function_exists( 'easy_mcp_ai_is_tool_call' ) && easy_mcp_ai_is_tool_call() ) {
        // This save was made by an AI tool call.
        update_post_meta( $post_id, '_edited_by_ai', time() );
    }
} );

The answer passes through the easy_mcp_ai_is_tool_call filter. Its second argument is the name of the tool running, for example wp_update_post, or null when none is:

php
apply_filters( 'easy_mcp_ai_is_tool_call', bool $is_tool_call, ?string $tool_name );

Check Whether the Request Is an MCP Request ​

The plugin defines EASY_MCP_AI_MCP_REQUEST as true when WordPress routes a POST, GET or DELETE request to the MCP endpoint (/wp-json/easy-mcp-ai/v1/mcp, with or without an API key in the path). easy_mcp_ai_is_mcp_request() returns the constant's value, or false when it is not defined, through the easy_mcp_ai_is_mcp_request filter:

php
apply_filters( 'easy_mcp_ai_is_mcp_request', bool $is_mcp_request );

Keep three things in mind:

  • It is set when WordPress routes the request, not earlier. It is not defined yet on plugins_loaded, init or rest_api_init. In exchange, a ?rest_route= request that WordPress serves from another route never sets it.
  • It says where the request went, not who sent it. The constant is defined before the API key or OAuth token is checked, so never treat it as proof that a caller is allowed to do anything.
  • A browser's CORS preflight does not set it. WordPress answers an OPTIONS request itself, before the plugin's handler runs.

If your site defines EASY_MCP_AI_MCP_REQUEST first, for example in wp-config.php, the plugin leaves that value alone.

Block a Tool Call Before It Runs ​

The easy_mcp_ai_before_tool_call action fires immediately before a tool runs, after every gate has passed: authentication, the key's or connection's tool list, the rate limit, the user's capability and approval. easy_mcp_ai_is_tool_call() is already true.

php
do_action( 'easy_mcp_ai_before_tool_call', string $tool_name, array $arguments, array $context );

Throw an exception to stop the call. The tool does not run, and your exception's message becomes the tool's error, so the AI client reads your reason. The message is cleaned the same way as any tool error before it is sent.

php
add_action( 'easy_mcp_ai_before_tool_call', function ( $tool, $args, $context ) {
    if ( 'wp_update_post' === $tool && isset( $args['status'] ) && 'publish' === $args['status'] ) {
        throw new RuntimeException( 'Publishing is frozen on this site until Monday.' );
    }
}, 10, 3 );

$arguments are the arguments the tool runs with. Sensitive values, such as passwords, tokens, secrets and any field a tool masks itself, arrive as '[REDACTED]', exactly as in the audit log.

$context describes the call:

KeyTypeMeaning
audit_idint or nullThe call's audit log row, or null while the audit log is off
parentstring or nullThe tool whose call this one runs inside, or null for a client's own call
depthint1 for a client's own call, 2 for a call a composite tool makes
compositeboolWhether this call is a composite tool that runs other tools
read_onlyboolWhether this call only reads
user_idintThe WordPress user the call runs as
auth_sourcestring'api_key' or 'oauth'
approval_idstring or nullThe approval this call redeemed, or null

Act After a Tool Call ​

The easy_mcp_ai_after_tool_call action fires once for every call that fired easy_mcp_ai_before_tool_call, whether the call succeeded or failed, so the two always pair up (see Limits for the one exception).

php
do_action( 'easy_mcp_ai_after_tool_call', string $tool_name, array $arguments, array $context );

It fires last: after the audit row and the change history are written, and after the call's change history has closed, so anything your listener writes is never recorded as the tool's change. For a client's own call, easy_mcp_ai_is_tool_call() is false here; for a call a composite tool made, it stays true until the composite tool finishes.

$arguments is the same array the before action received. $context has the same keys, plus these:

KeyTypeMeaning
statusstring'success' or 'error', as in the audit log
duration_msint or nullHow long the tool ran, or null when it never started
resultmixedWhat the tool returned, on success; null on error
errorstring or nullThe error text the client received, on error; null on success
php
add_action( 'easy_mcp_ai_after_tool_call', function ( $tool, $args, $context ) {
    if ( 'error' === $context['status'] ) {
        error_log( sprintf( 'AI tool %s failed after %s ms: %s', $tool, (string) $context['duration_ms'], $context['error'] ) );
    }
}, 10, 3 );

A listener that throws is logged when WP_DEBUG is on and otherwise ignored: it cannot turn a call that ran into a failed one. A tool that reports a problem by returning array( 'error' => … ) counts as 'success', as it does in the audit log.

Calls That Fire No Actions ​

CaseWhy
A call refused before it runs: the site is paused, or the tool is outside the key's list, rate-limited, beyond the user's capability, switched off or unknownNo tool ran
A call waiting for human approvalIt runs later, as its own call, once approved, and fires both actions then
A call whose arguments cannot be prepared, for example a third-party ability whose schema failsIt fails before the before action
A read wp_cli_run makes to plan a command, for example for the approval previewIt is not a call. easy_mcp_ai_is_tool_call() is true while it runs, but neither action fires. The command's real run fires both actions for each tool it calls.

Limits ​

LimitWhat happens
A tool that ends the PHP request itself (exit, wp_die() on a JSON request, a fatal error)easy_mcp_ai_after_tool_call does not fire, and easy_mcp_ai_is_tool_call() stays true for the rest of that request, shutdown hooks included. The audit row is still marked as an error.
A listener on the plugin's own cleanup throws, for example an easy_mcp_ai_tool_mutated listenereasy_mcp_ai_after_tool_call still fires, then the exception carries on. The cleanup it interrupted may not have finished, so the audit row or the change history can be incomplete.
Code that runs later, outside the tool's own PHP request (a WP-Cron event the tool scheduled, a queued job)It is not inside the call: both helpers answer for the request that eventually runs it.
Was this helpful?