Appearance
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
| Question | Use | True when |
|---|---|---|
| Is this request being served by the MCP endpoint? | easy_mcp_ai_is_mcp_request() or the EASY_MCP_AI_MCP_REQUEST constant | WordPress 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 call | The easy_mcp_ai_before_tool_call and easy_mcp_ai_after_tool_call actions | Every tool call that reaches execution |
| Run something after a tool changed the site | The easy_mcp_ai_tool_mutated action | A tool that writes has succeeded |
The two helpers answer different questions, so they can disagree:
| Moment | easy_mcp_ai_is_mcp_request() | easy_mcp_ai_is_tool_call() |
|---|---|---|
plugins_loaded, init or rest_api_init on an MCP request | false (the request has not been routed yet) | false |
| The client connecting, listing tools or authenticating | true | false |
Inside a tool, for example save_post fired by wp_update_post | true | true |
easy_mcp_ai_after_tool_call for the client's own call | true | false |
| A tool call run in the background by WP-Cron (a long task) | false | true |
| A visitor's page view, wp-admin or WP-CLI | false | false |
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,initorrest_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
OPTIONSrequest 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:
| Key | Type | Meaning |
|---|---|---|
audit_id | int or null | The call's audit log row, or null while the audit log is off |
parent | string or null | The tool whose call this one runs inside, or null for a client's own call |
depth | int | 1 for a client's own call, 2 for a call a composite tool makes |
composite | bool | Whether this call is a composite tool that runs other tools |
read_only | bool | Whether this call only reads |
user_id | int | The WordPress user the call runs as |
auth_source | string | 'api_key' or 'oauth' |
approval_id | string or null | The 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:
| Key | Type | Meaning |
|---|---|---|
status | string | 'success' or 'error', as in the audit log |
duration_ms | int or null | How long the tool ran, or null when it never started |
result | mixed | What the tool returned, on success; null on error |
error | string or null | The 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
| Case | Why |
|---|---|
| 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 unknown | No tool ran |
| A call waiting for human approval | It 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 fails | It fails before the before action |
A read wp_cli_run makes to plan a command, for example for the approval preview | It 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
| Limit | What 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 listener | easy_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. |