Skip to content

Logging: add a public API for recording MCP tool and ability requests - #914

Open
azizulhasan wants to merge 1 commit into
WordPress:developfrom
azizulhasan:add/public-request-logging-api
Open

Logging: add a public API for recording MCP tool and ability requests#914
azizulhasan wants to merge 1 commit into
WordPress:developfrom
azizulhasan:add/public-request-logging-api

Conversation

@azizulhasan

@azizulhasan azizulhasan commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

What?

Closes #906

Adds a public API so consumers that surface abilities themselves — an MCP server, or code invoking an ability directly — can record requests in the AI Request Logging experiment's log, instead of reaching into the experiment's internals or shipping a parallel log of their own.

  • Adds WordPress\AI\log_ai_request().
  • Adds AI_Request_Log_Manager::get_types() as the single source of truth for the supported log types, and derives the REST type enum from it.
  • AI_Request_Log_Manager::log() now rejects an unsupported type.
  • Fires wpai_ai_request_logged after a successful write.

Why?

The log's read contract already declares three types, but only one of them could ever exist:

  • AI_Request_Log_Controller::get_collection_params() exposes type with enum => array( '', 'ai_client', 'mcp_tool', 'ability' ).
  • The client mirrors it — LogEntry.type is typed 'ai_client' | 'mcp_tool' | 'ability' in src/admin/ai-request-logs/types.ts.
  • But the only writer is Log_Data_Extractor::extract_request_data(), which hardcodes 'type' => 'ai_client'. Nothing ever produced an mcp_tool or ability row.

There was also no supported way for anything outside the experiment to write one: AI_Request_Logging::$manager and ::get_manager() are private, and Logging_Integration::$log_manager is private static with no accessor. A consumer's only option was to construct its own AI_Request_Log_Manager, which bypasses the experiment-enabled check and re-runs maybe_upgrade_table() plus the cleanup cron negotiation on init().

The practical consequence: an MCP tool call that publishes a post is exactly the kind of AI-initiated request a site owner expects to find in Tools → AI Request Log, and it was invisible. Left unsolved centrally, every MCP or ability surface ends up shipping its own logging table.

How?

WordPress\AI\log_ai_request() in includes/helpers.php returns the log ID on success, or false when the experiment is disabled, so callers can invoke it unconditionally. It follows the namespaced convention used throughout that file (has_ai_credentials(), get_post_context(), …) rather than the wpai_-prefixed name sketched in the issue.

Logging_Integration::get_log_manager() exposes the shared manager, returning null when nothing has initialised it.

AI_Request_Log_Manager::get_types() returns the supported types, and AI_Request_Log_Controller::get_collection_params() now builds its enum from it, so the write and read sides cannot drift apart.

AI_Request_Log_Manager::log() validates type and refuses anything outside that set with _doing_it_wrong(). This is a behaviour change on a public method and worth calling out: a row carrying a type the REST API cannot filter is unreachable through the UI, so it is refused at the point of writing rather than stored where nothing can retrieve it. Any existing caller passing a custom type would begin losing rows.

One existing test changed. AI_Request_Log_ManagerTest::test_log_persists_entry() passed 'type' => 'ui', which was never one of the advertised values — a row typed ui can never be filtered through the REST API. It is updated to ai_client. Happy to revisit if ui was intentional.

Not included: validation is limited to type. Normalising other fields felt like scope creep for this issue.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Investigating the gap, drafting the implementation and the integration tests, and running the verification described below. I reviewed every line, decided the API shape and naming, and tested the change end-to-end on a local WordPress 7.1 site against a real MCP server before opening this PR.

Testing Instructions

Setup: enable the AI Request Logging experiment (the API intentionally no-ops while it is off).

1. A row can be recorded, and the reserved type is reachable

Drop this in an mu-plugin and load any admin page:

add_action( 'admin_init', function () {
	\WordPress\AI\log_ai_request( array(
		'type'      => 'mcp_tool',
		'operation' => 'example/tool-call',
		'status'    => 'success',
	) );
} );

Go to Tools → AI Request Log — the entry appears. Confirm the type by requesting /wp-json/ai/v1/logs?type=mcp_tool; before this change that filter could never match a row.

2. An unsupported type is refused

Change 'type' to 'not-a-real-type'. With WP_DEBUG on you get a _doing_it_wrong() notice, the call returns false, and no row is written.

3. It no-ops when the experiment is disabled

Turn the experiment off and reload. The call returns false and nothing is written — no fatal, no notice.

4. The write action fires

add_action( 'wpai_ai_request_logged', function ( $log_id, $data ) {
	error_log( "logged {$log_id} as {$data['type']}" );
}, 10, 2 );

Integration tests: tests/Integration/Includes/Logging/AI_Request_Log_ManagerTest.php and Log_Ai_RequestTest.php cover every supported type, rejection of unsupported and missing types, the action firing (and not firing when rejected), the inactive-experiment path, and that get_types() matches the REST enum.

Real-world check: I verified this against an MCP server exposing abilities on WordPress 7.1. On unpatched develop the MCP tool call ran and left no trace; with this branch the same call recorded an mcp_tool entry with a real duration, and a failing call recorded status: error carrying the actual WP_Error message.

Note: I could not run PHPUnit locally (no Docker on this machine), so the integration tests here have their first execution in CI. vendor/bin/phpcs and vendor/bin/phpstan analyse (level 8) both pass clean.

Screenshots or screencast

No UI changes. Entries render through the existing AI Request Log screen.

Changelog Entry

Added - Public WordPress\AI\log_ai_request() API so MCP servers and ability consumers can record requests in the AI Request Log.

Open WordPress Playground Preview

The request log's read contract already declared three log types — the REST
`type` collection param and the client `LogEntry` union both list `ai_client`,
`mcp_tool` and `ability` — but only `ai_client` could ever be produced, and the
log manager was private at every level, so nothing outside the experiment could
write a row.

Adds `WordPress\AI\log_ai_request()` so consumers that surface abilities
themselves, such as an MCP server, can record requests in the same log. It
returns false when the AI Request Logging experiment is disabled, so callers can
invoke it unconditionally.

Also:

- Adds `AI_Request_Log_Manager::get_types()` as the single source of truth for
  the supported types, and derives the REST enum from it so the read and write
  sides cannot drift.
- `AI_Request_Log_Manager::log()` now refuses an unsupported type with
  `_doing_it_wrong()`, since a row typed outside that set can never be filtered
  through the REST API.
- Fires `wpai_ai_request_logged` after a successful write so consumers can
  observe entries without polling.

An existing test passed `type => 'ui'`, which was never a supported value; it is
updated to `ai_client`.

See WordPress#906
@azizulhasan
azizulhasan requested a review from a team August 1, 2026 14:36
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: azizulhasan <hasanazizul@git.wordpress.org>
Co-authored-by: dkotter <dkotter@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.46154% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.45%. Comparing base (5599e9d) to head (4a2511d).

Files with missing lines Patch % Lines
includes/Logging/Logging_Integration.php 0.00% 2 Missing ⚠️
includes/Logging/AI_Request_Log_Manager.php 94.73% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop     #914      +/-   ##
=============================================
+ Coverage      80.43%   80.45%   +0.01%     
- Complexity      2565     2570       +5     
=============================================
  Files            110      110              
  Lines          10448    10472      +24     
=============================================
+ Hits            8404     8425      +21     
- Misses          2044     2047       +3     
Flag Coverage Δ
unit 80.45% <88.46%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@azizulhasan

Copy link
Copy Markdown
Contributor Author

CI is green except for one E2E test — settings.spec.js:264 › "Can turn on all experiments in a group" (188/189 passed). I think it's unrelated to this PR:

  • The call log contradicts itself: the locator resolved 14 times to <input checked type="checkbox" id="inspector-toggle-control-7" ...> while the assertion reported unexpected value "unchecked".
  • It failed on a different toggle each attempt — nth(1), then .first(), then nth(1) — rather than the same one consistently.
  • This PR is PHP-only under includes/Logging/ and includes/helpers.php; it touches no settings code, experiment registration, or JavaScript.

PHPCS, PHPStan and all 12 PHPUnit jobs (PHP 7.4–8.4, WP latest and trunk) pass.

I can't re-run the job from a fork — could someone with write access kick it off? Happy to dig further if it fails again in the same place.

@dkotter

dkotter commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

I can't re-run the job from a fork — could someone with write access kick it off? Happy to dig further if it fails again in the same place.

I've re-triggered that workflow so hopefully it passes. It is a flaky test, some fixes were added in #897 that we should maybe look to extract to it's own PR to get that merged in quicker

@jeffpaul jeffpaul added this to the 1.3.0 milestone Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Request Logging: no public API to record the reserved mcp_tool and ability log types

3 participants