Skip to content

Add MCP Access experiment: adapter auto-install and ability exposure controls - #992

Open
andreilupu wants to merge 5 commits into
WordPress:developfrom
andreilupu:add/mcp-adapter-experiment-autoinstall
Open

Add MCP Access experiment: adapter auto-install and ability exposure controls#992
andreilupu wants to merge 5 commits into
WordPress:developfrom
andreilupu:add/mcp-adapter-experiment-autoinstall

Conversation

@andreilupu

@andreilupu andreilupu commented Sep 2, 2026

Copy link
Copy Markdown

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:

  • Enabling the experiment installs and activates the MCP Adapter plugin from WordPress.org if it is not already active. The experiment description states this explicitly.
  • A Tools → MCP Access screen lists registered abilities with per-ability expose/hide checkboxes and a reset-to-default affordance, shows the MCP server endpoint URL, and surfaces the adapter's install state.

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/): standard Abstract_Feature experiment (mcp-adapter, admin category, no AI-capability requirement).

Auto-install (Plugin_Installer): runs on admin_init while the experiment is enabled; installs via core's plugins_api() + Plugin_Upgrader (silent skin) and activates. Guards: skips when already active; requires install_plugins/activate_plugins (which DISALLOW_FILE_MODS strips via map_meta_cap); a failed attempt sets a 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 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's wp/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 the wpai_mcp_exposed_abilities option as an ability-name → bool map; applied by injecting meta.mcp.public through the wp_register_ability_args filter (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 to McpAbilityExposure::is_meta_public() when the adapter is active so the screen always agrees with the server. POST persists overrides (null removes one); values are validated with rest_is_boolean and coerced via sanitize_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

  1. Enforcement layer for site-owner overrides. Registration-time injection has the limitations the adapter's own McpAbilityExposure docblock warns about: consumers that trigger ability registration before init:15 see unfiltered defaults for that request, later wp_register_ability_args callbacks can overwrite the injected meta, and the mutation is visible to every meta consumer. I checked whether the WP 7.1 wp_get_abilities() filtering (#64990, wp_get_abilities_item_include) could carry the overrides instead: it covers discovery (the default server lists via wp_get_abilities()), but not enforcementexecute-ability and get-ability-info fetch by name via wp_get_ability() and gate on McpAbilityExposure::is_public(), which reads meta unfiltered. Would a resolution-time filter in the adapter — e.g. apply_filters( 'mcp_ability_is_public', $resolved, $ability ) inside is_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.
  2. Relationship to Unifiied Abilities exposure controls #354. This screen is deliberately scoped to MCP exposure. If the unified "surfaces" model lands, the stored option can migrate into it — flagging so we align early rather than fork the UX.
  3. Disable semantics. Disabling the experiment stops enforcing overrides while the adapter keeps serving its defaults (the framework does not run disabled experiments' code). The description and screen state this explicitly. If reviewers prefer harder guarantees, that also lands on the resolution-time filter from question 1.
  4. Install timing. 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

  1. npm install && composer install && npm run wp-env start, npm run build.
  2. Since mcp-adapter is not on WordPress.org yet, point the installer at a stand-in plugin — e.g. as an mu-plugin:
    add_filter( 'wpai_mcp_adapter_plugin_slug', static fn (): string => 'hello-dolly' );
  3. Enable Settings → AI → Experiments → MCP Access. Note the description mentions the WordPress.org install.
  4. Load any wp-admin page: the stand-in plugin is installed from WordPress.org and activated automatically. (Deactivate + delete it and revisit to watch it again; as a non-admin or with DISALLOW_FILE_MODS, no install is attempted.)
  5. Visit Tools → MCP Access: toggle an ability's exposure, save, confirm the "Overridden / Reset to default" state; reset and confirm the developer default returns.
  6. With the real adapter active (e.g. mounted via .wp-env.override.json), confirm the endpoint URL row and that WP\MCP\Abilities\McpAbilityExposure::is_public() agrees with the screen for an overridden ability.
  7. npm run test:php -- --filter "MCP_Adapter|Settings_Controller|Plugin_Installer" — 24 tests.

Screenshots or screencast

Before After
(n/a — new screen) TODO: add screenshot of Tools → MCP Access (missing-plugin state + ability table)

Changelog Entry

Added - MCP Access experiment: installs and activates the MCP Adapter plugin from WordPress.org when enabled, and adds a Tools → MCP Access screen to control which abilities are exposed to AI agents over MCP.

Open WordPress Playground Preview

andreilupu and others added 4 commits September 2, 2026 21:07
…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>
@andreilupu
andreilupu requested a review from a team September 2, 2026 18:52
@github-actions

github-actions Bot commented Sep 2, 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: andreilupu <euthelup@git.wordpress.org>
Co-authored-by: dugyen <ugyensupport@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 Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.13580% with 75 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.06%. Comparing base (01d3970) to head (b9a2fea).

Files with missing lines Patch % Lines
...ludes/Experiments/MCP_Adapter/Plugin_Installer.php 47.36% 40 Missing ⚠️
...es/Experiments/MCP_Adapter/Settings_Controller.php 83.13% 14 Missing ⚠️
includes/Experiments/MCP_Adapter/Admin_Page.php 57.89% 8 Missing ⚠️
includes/Experiments/MCP_Adapter/MCP_Adapter.php 69.56% 7 Missing ⚠️
...des/Experiments/MCP_Adapter/Exposure_Overrides.php 85.71% 6 Missing ⚠️
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     
Flag Coverage Δ
unit 75.06% <69.13%> (-0.12%) ⬇️

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.

- 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>
@dugyen

dugyen commented Sep 2, 2026

Copy link
Copy Markdown

Tested this in WordPress Playground against the CI build artifact (pr-992-b9a2fea1...zip). Since mcp-adapter isn't on WordPress.org yet, I used the wpai_mcp_adapter_plugin_slug filter pointed at hello-dolly as the stand-in, per the testing instructions, and drove the flow via a custom blueprint (setSiteOptions for wpai_features_enabled / wpai_feature_mcp-adapter_enabled, plus a writeFile mu-plugin for the slug filter) to land directly on Tools → MCP Access.

Verified working:

  • Enabling the experiment auto-installs and activates the stand-in plugin (hello-dolly) with no manual step — confirmed via the plugin footer text and the plugin object in GET ai/v1/mcp/settings (status: "active", can_install/can_activate: true).
  • Tools → MCP Access renders the ability table correctly, populated with the three core abilities (core/get-site-info, core/get-user-info, core/get-environment-info), all exposed by default.
  • Toggling an ability's exposure off + Save changes persists correctly: POST ai/v1/mcp/settings succeeds, the row flips to "Overridden" with a "Reset to default" link, and the override survives a full page reload.
  • Reset to default correctly reverts the row to "Default" only after Save is clicked (client-only state until then) — overrides in the REST response goes back to {} and the ability reverts to its developer-declared default.
  • Graceful degradation when the real adapter class isn't present: adapter_active: false and endpoint: null in the settings response, rather than a fatal error — matches the "no hard dependency between the plugins" design described in the PR.
  • No PHP fatals or console errors surfaced during any of the above; the only console errors were expected CORS noise from Playground's own artifact-download retries.

Didn't test against the real mcp-adapter plugin itself (not published yet) so I can't confirm the McpAbilityExposure::is_public() agreement mentioned in step 6 of the testing instructions, or the actual MCP server endpoint URL rendering — that needs the real plugin mounted via .wp-env.override.json as noted.

Nice, clean implementation — the fallback behavior when the adapter isn't active is a good touch.

🤖 Generated with Claude Code

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.

2 participants