Add MCP Access experiment: adapter auto-install and ability exposure controls - #992
Add MCP Access experiment: adapter auto-install and ability exposure controls#992andreilupu wants to merge 5 commits into
Conversation
…reen Introduces the mcp-adapter experiment discussed in WordPress/mcp-adapter#178: - Experiment toggle registering MCP support in the AI plugin. - Tools > MCP Access screen listing registered abilities with per-ability expose/hide control, saved as overrides in wpai_mcp_exposed_abilities. - Overrides are injected into ability registration meta (meta.mcp.public) via wp_register_ability_args, so the MCP Adapter default server picks them up without a hard dependency between the plugins. - REST endpoint ai/v1/mcp/settings (GET/POST, manage_options) backing the screen, reporting adapter presence and the MCP endpoint URL. Auto-install/activation of the MCP Adapter plugin is deliberately left out until the adapter is published on WordPress.org. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Correctness fixes from an adversarially-verified review pass: - Report both effective and registration-default exposure per ability; stash the pre-override default when injecting overrides so it stays recoverable, and compute POST responses from the fresh overrides map instead of the stale registered-ability meta. - Resolve exposure by delegating to McpAbilityExposure::is_meta_public() when the adapter is active, keeping the screen in lockstep with what the server actually serves; local mirror remains only as fallback. - Derive the MCP endpoint URL from the adapter's registered servers instead of hardcoding the default route (handles re-routed or disabled default servers), and surface a notice when no server is registered. - Accept boolean-like strings in POST overrides (validate with rest_is_boolean, coerce with a sanitize_callback) so form-encoded clients are not silently rejected. - UI: overrides can now be cleared (per-row Reset to default sends the null-removal path; net-zero toggles no longer pin an override), save and load failures render recoverable inline notices instead of replacing the screen, and REST error messages surface via getErrorMessage(). - Honest metadata: description and docblock no longer claim the toggle wires up or gates the adapter; both state that overrides apply only while the experiment is enabled. The full disable-safety fix needs a resolution-time filter in mcp-adapter (to be proposed upstream). - Tests: assert the specific rest_api_init callback (was vacuous), plus coverage for default stashing, fresh null-removal responses, and string-boolean coercion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The settings payload now carries a plugin block (slug, install status, plugin file, capability flags) and the MCP Access screen offers an Install & activate / Activate button when the companion plugin is missing or inactive, driving WordPress core's wp/v2/plugins endpoint. The slug is filterable via wpai_mcp_adapter_plugin_slug so the flow can be exercised against a stand-in plugin (e.g. hello-dolly) until the adapter is published on WordPress.org. Buttons render only for users who hold install_plugins/activate_plugins, which DISALLOW_FILE_MODS already strips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follows the direction from WordPress/mcp-adapter#178: enabling the MCP Access experiment wires the site up without a separate install chore. - New Plugin_Installer runs on admin_init while the experiment is enabled: installs the companion plugin from WordPress.org via plugins_api() + Plugin_Upgrader (silent skin) and activates it. It skips silently when the plugin is active, the user lacks install_plugins/activate_plugins, or a recent attempt failed (1-hour transient lock so admin pages never stall on repeated downloads). A wpai_pre_mcp_adapter_autoinstall filter short-circuits the attempt for tests and externally-managed hosts. - The attempt runs on the first admin load after enabling rather than on the option change, because the experiments framework does not run disabled experiments' code in the request that enables them. - The experiment description now states plainly that enabling installs and activates a plugin from WordPress.org. - The screen's notice reflects the automatic flow, surfaces the last auto-attempt error (new autoinstall_error payload field), and keeps the manual button as the immediate retry path. - Plugin state resolution moved from Settings_Controller into Plugin_Installer::get_state() (single owner, controller delegates). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
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 #992 +/- ##
=============================================
- Coverage 75.18% 75.06% -0.12%
- Complexity 3227 3313 +86
=============================================
Files 133 138 +5
Lines 12488 12731 +243
=============================================
+ Hits 9389 9557 +168
- Misses 3099 3174 +75
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:
|
- Guard plugins_api()'s object|array return before reading download_link, with a proper error when it is absent. - Give get_endpoint_url() its own class_exists guard instead of relying on the caller's, and pass rest_sanitize_boolean() a typed value. - Correct the get_state() return docblock (autoinstall_error was missing) and string-cast the plugin file from array_keys(). - Multisite test fixes: install_plugins/activate_plugins belong to super admins on multisite (the product correctly hides the install flow from site admins there), so the installer tests now use a super-admin-capable user and the install-state assertion grants and revokes super admin around the request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Tested this in WordPress Playground against the CI build artifact ( Verified working:
Didn't test against the real Nice, clean implementation — the fallback behavior when the adapter isn't active is a good touch. 🤖 Generated with Claude Code |
What?
See WordPress/mcp-adapter#178 (AI-plugin integration discussed in the comments); related to #354.
Adds an MCP Access experiment that wires a site up to the MCP Adapter companion plugin and gives site owners a place to control which abilities are exposed to AI agents over the Model Context Protocol:
Why?
Discussed in WordPress/mcp-adapter#178: the adapter stays a standalone, headless plugin (reusable by other consumers, no admin UI of its own — see WordPress/mcp-adapter#184), while the AI plugin provides the user-facing control layer. Abilities come from arbitrary plugins; site owners need a clear place to decide what is reachable by AI agents (the same need behind #354).
How?
Experiment (
includes/Experiments/MCP_Adapter/): standardAbstract_Featureexperiment (mcp-adapter, admin category, no AI-capability requirement).Auto-install (
Plugin_Installer): runs onadmin_initwhile the experiment is enabled; installs via core'splugins_api()+Plugin_Upgrader(silent skin) and activates. Guards: skips when already active; requiresinstall_plugins/activate_plugins(whichDISALLOW_FILE_MODSstrips viamap_meta_cap); a failed attempt sets a 1-hour transient lock so admin pages never stall on repeated downloads; awpai_pre_mcp_adapter_autoinstallfilter short-circuits the attempt for tests and externally managed hosts. The attempt runs on the first admin load after enabling because the experiments framework does not run disabled experiments' code in the request that enables them. While the plugin is missing/inactive, the screen shows the state, the last auto-attempt error if any, and a manual install/activate button (driving core'swp/v2/plugins) as the immediate retry path. The slug is filterable (wpai_mcp_adapter_plugin_slug) so the flow can be exercised before the adapter is published on WordPress.org.Exposure overrides (
Exposure_Overrides): stored in thewpai_mcp_exposed_abilitiesoption as an ability-name → bool map; applied by injectingmeta.mcp.publicthrough thewp_register_ability_argsfilter (priority 100). The adapter's default server resolves exposure from exactly that key (McpAbilityExposure), so overrides take effect with no hard dependency between the plugins. The registration-time default is stashed before injection so the screen can always report and restore it. Abilities without an override keep their developer-declared visibility.Settings REST (
Settings_Controller,ai/v1/mcp/settings,manage_options): GET returns adapter/plugin state, endpoint URL (read from the adapter's registered servers, not hardcoded), and per-ability effective + default exposure — delegating toMcpAbilityExposure::is_meta_public()when the adapter is active so the screen always agrees with the server. POST persists overrides (nullremoves one); values are validated withrest_is_booleanand coerced viasanitize_callback, so form-encoded clients work.UI (
src/experiments/mcp-adapter/): React screen following the existing experiment app patterns; recoverable inline error notices,getErrorMessage()for REST errors.Open questions for reviewers
McpAbilityExposuredocblock warns about: consumers that trigger ability registration beforeinit:15see unfiltered defaults for that request, laterwp_register_ability_argscallbacks can overwrite the injected meta, and the mutation is visible to every meta consumer. I checked whether the WP 7.1wp_get_abilities()filtering (#64990,wp_get_abilities_item_include) could carry the overrides instead: it covers discovery (the default server lists viawp_get_abilities()), but not enforcement —execute-abilityandget-ability-infofetch by name viawp_get_ability()and gate onMcpAbilityExposure::is_public(), which reads meta unfiltered. Would a resolution-time filter in the adapter — e.g.apply_filters( 'mcp_ability_is_public', $resolved, $ability )insideis_public()— be acceptable? It would give site-owner controls (and eventually the "surfaces" idea from Unifiied Abilities exposure controls #354) a reliable enforcement point at the adapter layer, matching what 7.1 did for queries at the core layer. I'm happy to PR it to mcp-adapter; this PR's option format and screen would carry over unchanged.admin_init(first admin load after enabling) vs. hooking the settings-save moment — happy to change if there's a preference.Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Fable 5
Used for: Implementation, integration tests, and an adversarial self-review pass (which produced the hardening in the second commit). All code was reviewed, exercised locally, and is submitted under my responsibility.
Testing Instructions
npm install && composer install && npm run wp-env start,npm run build.mcp-adapteris not on WordPress.org yet, point the installer at a stand-in plugin — e.g. as an mu-plugin:DISALLOW_FILE_MODS, no install is attempted.).wp-env.override.json), confirm the endpoint URL row and thatWP\MCP\Abilities\McpAbilityExposure::is_public()agrees with the screen for an overridden ability.npm run test:php -- --filter "MCP_Adapter|Settings_Controller|Plugin_Installer"— 24 tests.Screenshots or screencast
Changelog Entry