Logging: add a public API for recording MCP tool and ability requests - #914
Logging: add a public API for recording MCP tool and ability requests#914azizulhasan wants to merge 1 commit into
Conversation
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
|
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 If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
CI is green except for one E2E test —
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. |
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 |
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.
WordPress\AI\log_ai_request().AI_Request_Log_Manager::get_types()as the single source of truth for the supported log types, and derives the RESTtypeenum from it.AI_Request_Log_Manager::log()now rejects an unsupportedtype.wpai_ai_request_loggedafter 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()exposestypewithenum => array( '', 'ai_client', 'mcp_tool', 'ability' ).LogEntry.typeis typed'ai_client' | 'mcp_tool' | 'ability'insrc/admin/ai-request-logs/types.ts.Log_Data_Extractor::extract_request_data(), which hardcodes'type' => 'ai_client'. Nothing ever produced anmcp_toolorabilityrow.There was also no supported way for anything outside the experiment to write one:
AI_Request_Logging::$managerand::get_manager()are private, andLogging_Integration::$log_managerisprivate staticwith no accessor. A consumer's only option was to construct its ownAI_Request_Log_Manager, which bypasses the experiment-enabled check and re-runsmaybe_upgrade_table()plus the cleanup cron negotiation oninit().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()inincludes/helpers.phpreturns the log ID on success, orfalsewhen 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 thewpai_-prefixed name sketched in the issue.Logging_Integration::get_log_manager()exposes the shared manager, returningnullwhen nothing has initialised it.AI_Request_Log_Manager::get_types()returns the supported types, andAI_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()validatestypeand 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 typeduican never be filtered through the REST API. It is updated toai_client. Happy to revisit ifuiwas 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:
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'. WithWP_DEBUGon you get a_doing_it_wrong()notice, the call returnsfalse, and no row is written.3. It no-ops when the experiment is disabled
Turn the experiment off and reload. The call returns
falseand nothing is written — no fatal, no notice.4. The write action fires
Integration tests:
tests/Integration/Includes/Logging/AI_Request_Log_ManagerTest.phpandLog_Ai_RequestTest.phpcover every supported type, rejection of unsupported and missing types, the action firing (and not firing when rejected), the inactive-experiment path, and thatget_types()matches the REST enum.Real-world check: I verified this against an MCP server exposing abilities on WordPress 7.1. On unpatched
developthe MCP tool call ran and left no trace; with this branch the same call recorded anmcp_toolentry with a real duration, and a failing call recordedstatus: errorcarrying the actualWP_Errormessage.Screenshots or screencast
No UI changes. Entries render through the existing AI Request Log screen.
Changelog Entry