Inspector, from the previous lesson, shows you what happened on a call you triggered yourself. It can’t tell you what happened on a call a real AI client made five minutes ago, or an hour ago, or in production while you weren’t watching. For that, you need the adapter’s own error handler, what it captures by default, and how to extend it so a failure leaves you more than a single line in a shared log file.
A working MCP server with at least one ability (Course 2). Comfort implementing a PHP interface and reading wp-content/debug.log. If you’ve already built the audit log pattern from Course 4, this lesson builds directly on it.
Step 1: What ErrorLogMcpErrorHandler actually does
ErrorLogMcpErrorHandler is the MCP Adapter’s default implementation of
McpErrorHandlerInterface, and it does exactly what its name says: it writes failures
to PHP’s error log, with structured context attached, rather than to any WordPress
database table or admin screen. The interface itself is small, a single method:
// File: illustrative, the interface's shape
interface McpErrorHandlerInterface {
public function log( string $message, array $context = array(), string $type = 'error' ): void;
}
When something fails inside a tool call, the default handler’s log() call ends up in
wp-content/debug.log as a structured line, roughly:
[ERROR] Tool execution failed | Context: {"tool_name":"my-plugin-create-draft-post"} | User ID: 123
This is the same distinction Course 4’s audit log lesson makes, worth repeating here
because it’s exactly why this lesson exists as a companion to that one.
ErrorLogMcpErrorHandler logs failures. A fully successful, fully permitted “delete
this post” call from an agent produces no entry here at all. If you need a record of
what succeeded, not just what failed, that’s the audit log pattern from Course 4, not
this error handler.
Step 2: Reading an entry correctly
Each logged entry gives you the three things Lesson 1’s isolation method needs: which tool, which user, and what kind of failure. Read them in that order when you’re working backward from a log entry to a cause:
A message mentioning schema validation points you back to Lesson 5. A message
mentioning a permission or capability check points you at permission_callback
directly. A raw PHP exception message and stack trace points at your
execute_callback’s own logic. The log entry is doing the first pass of the isolation
work for you, if you read it before guessing.
Step 3: Building a custom error handler for more visibility
The default handler’s biggest limitation isn’t what it captures, it’s where it puts it: a single shared PHP error log, mixed in with every other plugin’s and WordPress core’s own errors, with no easy way to query “show me every MCP failure from the last day.” Implementing the same interface yourself gives you a dedicated destination:
// File: my-plugin.php
class My_Plugin_Mcp_Error_Handler implements McpErrorHandlerInterface {
public function log( string $message, array $context = array(), string $type = 'error' ): void {
global $wpdb;
$wpdb->insert( $wpdb->prefix . 'mcp_error_log', array(
'type' => $type,
'message' => $message,
'context' => wp_json_encode( $context ),
'user_id' => get_current_user_id(),
'created_at' => current_time( 'mysql' ),
) );
// Still write to the normal error log too, don't lose the default behavior.
error_log( sprintf( '[MCP %s] %s | %s', strtoupper( $type ), $message, wp_json_encode( $context ) ) );
}
}
Wire it into your server’s configuration the same way Course 4’s audit log lesson wires in a custom observability handler, through the adapter’s server config filter:
// File: my-plugin.php
add_filter( 'mcp_adapter_default_server_config', function ( $config ) {
$config['error_handler'] = My_Plugin_Mcp_Error_Handler::class;
return $config;
} );
Use the same dbDelta() table-creation pattern from Course 4’s audit log lesson to
create mcp_error_log on plugin activation, a plain LONGTEXT column for context,
a VARCHAR for type, and an index on user_id and created_at so you can actually
query it.
Wrap any I/O in your custom log() method (a database insert, an HTTP call to an
external alerting service) so that a failure inside your error handler itself doesn’t
throw a new, unrelated fatal error on top of the one you were trying to record. Catch
and swallow, falling back to a plain error_log() call, rather than letting logging
become a second point of failure.
Step 4: How this relates to the audit log pattern from Course 4
Course 4 built an audit log by wrapping execute_callback directly, capturing every
call, successful or not, with its arguments and timing. This lesson’s custom error
handler is a complementary, narrower layer: it only fires on failures, but it captures
them consistently across every ability on the server without needing you to remember to
wrap each one individually. In practice, run both: the audit log wrapper for a complete
record of what agents actually did, and a custom error handler for a fast, queryable
view specifically of what went wrong, which is usually the first table you’ll query
while debugging.
Same caution as Course 4’s audit log lesson gives for the observability handler: the
log() method’s signature is stable, but exactly which messages and context keys get
passed to it can evolve between adapter versions. Temporarily error_log() the raw
$message, $context, and $type your handler receives during development, so you
know precisely what’s available before building any dashboard or alert on top of
specific context keys.
Test it: confirm your custom handler actually receives failures
Trigger a deliberate failure, call an ability with input you know violates its schema, then confirm your table captured it:
# File: terminal
wp eval '
global $wpdb;
$rows = $wpdb->get_results( "SELECT type, message, user_id, created_at FROM {$wpdb->prefix}mcp_error_log ORDER BY id DESC LIMIT 5" );
print_r( $rows );
'
You should see the failure you just triggered, with the correct type and user ID, confirming your handler is wired in and actually receiving events, not just present in your code.
Recap
ErrorLogMcpErrorHandler is the adapter’s default McpErrorHandlerInterface
implementation, it writes failures, not successes, to the PHP error log with tool name
and user context attached. Read the tool_name, user ID, and message fields in that
order to jump straight to the right layer from Lesson 1’s isolation method. A custom
implementation, wired in through the same server config filter Course 4 uses for its
observability handler, gives you a dedicated, queryable destination. Run it alongside,
not instead of, the audit log pattern from Course 4, since one covers failures and the
other covers everything.
Resources & further reading
- MCP Adapter repository, error handling guide, GitHub
- MCP Adapter repository, observability guide, GitHub
- MCP Adapter repository, GitHub
- $wpdb class reference, developer.wordpress.org