There is no admin screen in the MCP Adapter showing “which agent called what tool,
when, and with what result.” The adapter ships an error handler
(ErrorLogMcpErrorHandler, writing failures to WordPress’s error log) and an
observability extension point (McpObservabilityHandlerInterface, whose default
implementation, NullMcpObservabilityHandler, records nothing at all). Neither is an
audit trail. If you need one, and for production or enterprise use you should, you build
it yourself. This lesson covers two real, workable approaches.
A working MCP server with at least one registered ability (Course 2). Comfort writing a WordPress plugin activation hook and using $wpdb.
Step 1: What’s genuinely built in, and what isn’t
It’s an easy mistake to see ErrorLogMcpErrorHandler in the adapter and assume
successful, legitimate tool calls are being tracked somewhere too. They aren’t. A
successful, fully-permitted “delete this post” call from an agent leaves no built-in
trace at all unless you add one.
Step 2: The most reliable approach, wrap execute_callback yourself
Because you already write every ability’s execute_callback, wrapping it gives you
exact control over what gets recorded, the ability name, the authenticated user, the
arguments, the result, and the timing, all in one place:
// File: my-plugin.php
function my_plugin_audited_execute( string $ability_name, callable $callback ): callable {
return function ( $input ) use ( $ability_name, $callback ) {
$start = microtime( true );
$user = get_current_user_id();
$result = null;
$error = null;
try {
$result = $callback( $input );
} catch ( \Throwable $e ) {
$error = $e->getMessage();
throw $e;
} finally {
my_plugin_log_agent_action( array(
'ability' => $ability_name,
'user_id' => $user,
'input' => wp_json_encode( $input ),
'error' => $error,
'duration_ms' => round( ( microtime( true ) - $start ) * 1000, 2 ),
) );
}
return $result;
};
}
Then wrap it at registration time, around whatever the real logic already is:
// File: my-plugin.php
add_action( 'wp_abilities_api_init', function () {
wp_register_ability(
'my-plugin/update-own-draft',
array(
// ...label, input_schema, permission_callback as before...
'execute_callback' => my_plugin_audited_execute(
'my-plugin/update-own-draft',
function ( $input ) {
wp_update_post( array( 'ID' => $input['post_id'], 'post_content' => $input['content'] ) );
return array( 'updated' => true );
}
),
)
);
} );
Every call, successful or not, now produces exactly one log entry with the fields your own audit needs, without depending on any adapter-internal event structure.
Step 3: A real table to store it in
// File: my-plugin.php
function my_plugin_create_audit_table() {
global $wpdb;
$table = $wpdb->prefix . 'ai_agent_audit_log';
$charset = $wpdb->get_charset_collate();
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( "CREATE TABLE {$table} (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
ability VARCHAR(191) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
input LONGTEXT NULL,
error TEXT NULL,
duration_ms FLOAT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY ability (ability),
KEY user_id (user_id)
) {$charset};" );
}
register_activation_hook( __FILE__, 'my_plugin_create_audit_table' );
function my_plugin_log_agent_action( array $entry ) {
global $wpdb;
$wpdb->insert( $wpdb->prefix . 'ai_agent_audit_log', array(
'ability' => $entry['ability'],
'user_id' => $entry['user_id'],
'input' => $entry['input'],
'error' => $entry['error'],
'duration_ms' => $entry['duration_ms'],
'created_at' => current_time( 'mysql' ),
) );
}
The same care from Lesson 6 applies here. If an ability’s input includes anything
sensitive, redact it before it’s written into wp_json_encode( $input ), an audit log
that itself stores unredacted PII is a new place that data can leak from.
Step 4: Wiring the adapter’s own observability handler too
The MCP Adapter’s McpObservabilityHandlerInterface gives you a second, complementary
layer, lower-level events from the adapter’s own request routing, useful for metrics
like request volume and duration across your whole server, not just individual
abilities you’ve wrapped:
// File: my-plugin.php
class My_Plugin_Observability_Handler implements McpObservabilityHandlerInterface {
public function record_event( string $event, array $tags = array(), ?float $duration_ms = null ): void {
global $wpdb;
$wpdb->insert( $wpdb->prefix . 'ai_agent_audit_log', array(
'ability' => $event,
'user_id' => get_current_user_id(),
'input' => wp_json_encode( $tags ),
'duration_ms' => $duration_ms,
'created_at' => current_time( 'mysql' ),
) );
}
}
add_filter( 'mcp_adapter_default_server_config', function ( $config ) {
$config['observability_handler'] = My_Plugin_Observability_Handler::class;
return $config;
} );
The interface’s method signature, record_event( string $event, array $tags = [], ?float $duration_ms = null ),
is stable, but exactly which event names and tag keys the adapter’s request router
passes in can evolve between adapter versions. During development, temporarily
error_log() the raw $event and $tags your handler receives so you know precisely
what’s available before building dashboards or alerts on top of specific tag keys.
Test it: confirm entries actually land
wp-env run cli wp eval '
global $wpdb;
$rows = $wpdb->get_results( "SELECT ability, user_id, duration_ms, created_at FROM {$wpdb->prefix}ai_agent_audit_log ORDER BY id DESC LIMIT 5" );
print_r( $rows );
'
Trigger an ability call from your connected AI client, then re-run the query. You should see a new row with the correct ability name and user ID.
Recap
Nothing built into the MCP Adapter records successful agent tool calls. Wrapping your
own execute_callback functions gives you the most precise, reliable audit trail, since
you control exactly what gets logged, and a custom McpObservabilityHandlerInterface
implementation adds a complementary, adapter-level layer for broader request metrics.
Store both in a real database table, and redact sensitive arguments before they’re
written, the same discipline as everywhere else in this course.
Resources & further reading
- MCP Adapter repository, observability guide, GitHub
- MCP Adapter repository, error handling guide, GitHub
- $wpdb class reference, developer.wordpress.org