Skip to content

feat(admin): add Settings > MCP Adapter page to opt registered abilities into the default server (fixes #183)#184

Open
webmyc wants to merge 4 commits into
WordPress:trunkfrom
respira-press:feat/issue-183-default-ability-opt-in-ui
Open

feat(admin): add Settings > MCP Adapter page to opt registered abilities into the default server (fixes #183)#184
webmyc wants to merge 4 commits into
WordPress:trunkfrom
respira-press:feat/issue-183-default-ability-opt-in-ui

Conversation

@webmyc

@webmyc webmyc commented May 16, 2026

Copy link
Copy Markdown

This PR addresses #183 by adding a first-class admin settings page under Settings → MCP Adapter that lets site administrators expose registered WordPress abilities to the default MCP server without writing PHP filters.

Background

The predecessor plugin Automattic/wordpress-mcp shipped with a UI toggle ("Enable REST API CRUD Tools") that did this work for end users. When the canonical adapter replaced it, that affordance was lost. Issue #183 documents the regression.

In the meantime, the workaround has been to write the wp_register_ability_args filter documented on WordPress contributor blogs and several developer guides. Third-party helper plugins exist but are inconsistent.

What this PR adds

  • A new settings page at Settings → MCP Adapter, accessible to users with manage_options.
  • Per-ability checkboxes to expose registered abilities to the default MCP server's public surface.
  • Persistence via a single option: mcp_adapter_public_abilities.
  • A wp_register_ability_args filter that applies the saved selection at registration time. Same hook the documented workaround uses, so existing developer code is unaffected.
  • A unit test for the filter behavior (tests/Unit/Admin/AbilityExposureFilterTest.php).
  • A short "Settings page" section in README.md under "Basic Usage".

What this PR does not add

  • No multi-server UI. Default server only.
  • No CRUD preset bundles.
  • No React-based UI. Plain PHP and standard wp-admin patterns.
  • No annotation-aware warning for destructive abilities.
  • No source filter, no bulk actions, no connection guides. Out of scope for upstream.

A working reference implementation

A more feature-complete version of this exact pattern is shipped as a standalone plugin at https://github.com/respira-press/inhale-mcp-abilities.

That plugin includes the additional features this PR omits for scope reasons (annotation-aware destructive confirmation, source filter, bulk actions, light/dark theme, connection guides). It serves as a reference for what a fully-realized UI for this feature could look like, and as a usable solution for site owners while the upstream PR goes through review.

Compatibility

  • WordPress: tested on 7.0-RC4.
  • PHP: 7.4 minimum, declared via declare(strict_types=1);.
  • No new Composer dependencies.
  • No new npm dependencies.
  • No changes to the public PHP API of any existing class.

Manual test plan

  1. Activate mcp-adapter on a WordPress 7.0 site.
  2. Confirm Settings → MCP Adapter appears for an administrator.
  3. Confirm a non-admin user cannot access the page.
  4. Select 2-3 abilities, save.
  5. Confirm get_option( 'mcp_adapter_public_abilities' ) returns those abilities.
  6. Confirm wp_get_ability( 'core/get-site-info' )->get_meta() (or equivalent) returns meta.mcp.public = true for the selected ones.
  7. Deselect one, save, confirm it's removed.
  8. Run npm run lint:php, npm run lint:php:stan, npm run test:php. All green.

Follow-ups suggested for separate PRs

  • Annotation-aware warning when destructive abilities are checked.
  • Preset bundles for core CRUD abilities once WP 7.0 ships stable namespaces.
  • React-based UI under @wordpress/scripts.
  • Per-server configuration UI for custom MCP servers.

Notes for reviewers

This is a first contribution to mcp-adapter. Happy to iterate on scope, naming, or implementation patterns. The standalone respira-press/inhale-mcp-abilities plugin exists to keep momentum during review while still demonstrating the architecture.

…ies into the default server (fixes WordPress#183)

The default MCP server hides every registered ability by default, requiring
site administrators to write the wp_register_ability_args filter documented
in WordPress contributor guides. This adds a first-class settings page
under Settings > MCP Adapter that exposes a per-ability checkbox UI and
persists the selection in a single `mcp_adapter_public_abilities` option.

The page is deliberately minimal:
- Standard wp-admin h1 + introductory paragraph
- One list table of registered abilities with checkboxes
- One option, one filter
- No React, no annotation-aware UI, no bulk actions, no source filter
- PHP 7.4, namespaced, strict_types

A more feature-complete reference implementation of the same pattern is
shipped as a standalone plugin at
https://github.com/respira-press/inhale-mcp-abilities for site owners who
want the additional affordances while this PR is in review.

The developer-facing path (setting meta.mcp.public = true at registration
time) remains the canonical primitive; the settings page is positioned
for site owners who don't write PHP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 16, 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: webmyc <urbankidro@git.wordpress.org>

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an admin-facing configuration surface to let site administrators opt registered WordPress abilities into exposure on the default MCP server (via meta.mcp.public = true), addressing the UX regression described in #183 while preserving the existing developer-first filter workflow.

Changes:

  • Introduces Settings → MCP Adapter wp-admin page that lists registered abilities and persists opted-in ability names in the mcp_adapter_public_abilities option.
  • Adds a wp_register_ability_args filter that reads the saved option and injects meta.mcp.public = true for opted-in abilities (excluding the adapter’s managed namespace).
  • Adds a unit test covering the filter behavior and documents the new settings page in the README.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
includes/Plugin.php Wires up the new admin settings page (admin-only) and registers the exposure filter on plugin init.
includes/Admin/AbilityExposureFilter.php Implements wp_register_ability_args filter to set meta.mcp.public for opted-in abilities.
includes/Admin/SettingsPage.php Adds wp-admin Settings page, option registration, sanitization, and ability discovery UI.
tests/Unit/Admin/AbilityExposureFilterTest.php Adds unit tests validating exposure filter behavior and meta preservation.
README.md Documents the new Settings page behavior and how it relates to the canonical developer flow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread includes/Admin/SettingsPage.php Outdated
</thead>
<tbody>
<?php foreach ( $abilities as $name => $info ) : ?>
<tr<?php echo $info['managed'] ? ' class="disabled"' : ''; ?>>

@webmyc webmyc May 17, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Computed the value into a $row_class string and echoed it through esc_attr(). Pushed in 8cb5be2.

Comment thread includes/Admin/SettingsPage.php Outdated
*/
public function render_page(): void {
if ( ! current_user_can( self::CAPABILITY ) ) {
wp_die( esc_html__( 'You do not have permission to access this page.', 'mcp-adapter' ) );

@webmyc webmyc May 17, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. wp_die() now passes response: 403 and back_link: true so the authorization failure is reflected correctly in HTTP status and access logs. The denial behavior is unchanged otherwise. Pushed in 8cb5be2.

webmyc added a commit to respira-press/mcp-adapter that referenced this pull request May 17, 2026
- Escape the row class attribute: replace the inline ternary that
  conditionally injected ` class="disabled"` with a computed
  `$row_class` string echoed through esc_attr(). Resolves the
  WordPress.Security.EscapeOutput risk Copilot flagged at line 204.
- wp_die() in render_page() now passes response code 403 + back_link
  so automated clients and access logs reflect the authorization
  failure correctly instead of falling back to a generic error.

Both changes are local to includes/Admin/SettingsPage.php. No behavior
change beyond the response code; the same denial still blocks the
render path when current_user_can( manage_options ) is false.

Refs Copilot review on PR WordPress#184.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
webmyc added a commit to respira-press/mcp-adapter that referenced this pull request May 17, 2026
- Escape the row class attribute: replace the inline ternary that
  conditionally injected ` class="disabled"` with a computed
  `$row_class` string echoed through esc_attr(). Resolves the
  WordPress.Security.EscapeOutput risk Copilot flagged at line 204.
- wp_die() in render_page() now passes response code 403 + back_link
  so automated clients and access logs reflect the authorization
  failure correctly instead of falling back to a generic error.

Both changes are local to includes/Admin/SettingsPage.php. No behavior
change beyond the response code; the same denial still blocks the
render path when current_user_can( manage_options ) is false.

Refs Copilot review on PR WordPress#184.
@webmyc webmyc force-pushed the feat/issue-183-default-ability-opt-in-ui branch from e8a7fcd to 41f12a6 Compare May 17, 2026 11:00
webmyc pushed a commit to respira-press/mcp-adapter that referenced this pull request May 17, 2026
- Escape the row class attribute: replace the inline ternary that
  conditionally injected ` class="disabled"` with a computed
  `$row_class` string echoed through esc_attr(). Resolves the
  WordPress.Security.EscapeOutput risk Copilot flagged at line 204.
- wp_die() in render_page() now passes response code 403 + back_link
  so automated clients and access logs reflect the authorization
  failure correctly instead of falling back to a generic error.

Both changes are local to includes/Admin/SettingsPage.php. No behavior
change beyond the response code; the same denial still blocks the
render path when current_user_can( manage_options ) is false.

Refs Copilot review on PR WordPress#184.

Co-authored-by: webmyc <urbankidro@git.wordpress.org>
@webmyc webmyc force-pushed the feat/issue-183-default-ability-opt-in-ui branch from 41f12a6 to 017ab9c Compare May 17, 2026 11:09
- Escape the row class attribute: replace the inline ternary that
  conditionally injected ` class="disabled"` with a computed
  `$row_class` string echoed through esc_attr(). Resolves the
  WordPress.Security.EscapeOutput risk Copilot flagged at line 204.
- wp_die() in render_page() now passes response code 403 + back_link
  so automated clients and access logs reflect the authorization
  failure correctly instead of falling back to a generic error.

Both changes are local to includes/Admin/SettingsPage.php. No behavior
change beyond the response code; the same denial still blocks the
render path when current_user_can( manage_options ) is false.

Refs Copilot review on PR WordPress#184.

Co-authored-by: webmyc <urbankidro@git.wordpress.org>
@webmyc webmyc force-pushed the feat/issue-183-default-ability-opt-in-ui branch from 017ab9c to 8cb5be2 Compare May 17, 2026 11:10
@webmyc

webmyc commented May 18, 2026

Copy link
Copy Markdown
Author

Copilot review feedback is in (commit 8cb5be2):

  • Row-class attribute in the abilities table now computes a $row_class string and echoes it via esc_attr(). No conditional attribute fragment injection, conforms to WordPress.Security.EscapeOutput.
  • wp_die() on the permission-denied path now passes response: 403 and back_link: true so the authorization failure surfaces correctly in HTTP status and access logs.

The downstream Inhale plugin, which distributes the same pattern as a standalone wp.org-bound option for sites that don't run the adapter directly, just shipped v0.2.0 today using the canonical mcp_adapter_public_abilities option key proposed here, with a one-shot migration from the legacy Inhale-prefixed key. So both surfaces will read and write the same option once this lands.

No rush on review; happy to iterate on scope, naming or file structure.

webmyc added a commit to respira-press/inhale-mcp-abilities that referenced this pull request May 19, 2026
…, fix Contributors

Addresses all three issues raised in the initial wp.org Plugin Directory
review (review ID F1 inhale-mcp-abilities/urbankidro/19May26).

1. Prefix every identifier with respira_inhale_* / Respira_Inhale_* /
   RESPIRA_INHALE_*. Constants, classes, functions, filters, the nonce
   action and the CSS row-id prefix all carry the prefix now. Class
   files renamed to class-respira-inhale-*.php to match. The new
   prefix is unique to this plugin and does NOT collide with the bare
   respira_* namespace used by the main Respira for WordPress plugin.

2. Properly prefix the option key. Primary storage moves from the
   generic mcp_adapter_public_abilities to respira_inhale_public_abilities.
   The plugin still mirrors writes to mcp_adapter_public_abilities (the
   canonical compat key proposed first-party in WordPress/mcp-adapter#184)
   and reads it as a fallback, so if the upstream adapter ships its own
   settings UI later under that key, both surfaces share state with no
   migration required. A one-shot migration on plugin upgrade copies any
   prior selection (v0.1.x legacy key or v0.2.x-v0.3.x canonical-only
   key) onto the new prefixed key and removes the old options. Tested
   end-to-end on Studio: a v0.3.2 install with a populated selection
   list migrates to v0.4.0 with the full list preserved on the new key
   and every legacy key cleared.

3. Replace home_url() + hardcoded /wp-json path with rest_url() for the
   Connection section endpoint. The new code uses
   `rest_url('mcp/mcp-adapter-default-server')` so the REST base is
   resolved by WordPress core for sub-directory installs, custom
   permalink structures, multisite blogs, etc.

Bonus cleanups:
- Display name updated to "Inhale: MCP Abilities by Respira" so the
  author attribution is visible in wp-admin plugin lists. Slug
  inhale-mcp-abilities is unchanged (already approved with the wp.org
  reviewer).
- readme.txt Contributors field now lists urbankidro (the wp.org
  username that owns the plugin) instead of "respira" (the brand
  string, which is not a registered wp.org username). The Author
  plugin header still reads "Respira" so the visible attribution on
  the directory page is unchanged.
- uninstall.php updated to delete the new primary key, the v0.4.0
  migration flag, the canonical compat key, and every legacy key
  from v0.1.x and v0.2.x. Single-site and multisite sweep.
- tests/Unit/Admin/AbilityExposureFilterTest.php: parent declares
  set_up() public, so the override must match visibility (LSP). This
  was the cause of "Access level to set_up() must be public" fatal
  across every PHP / WP matrix combo.
- includes/Plugin.php: flip the is_admin() guard in setup() into an
  early-return so SlevomatCodingStandard.ControlStructures.EarlyExit
  no longer fires on line 68.
- includes/Admin/SettingsPage.php: drop the redundant is_array() guard
  on wp_get_abilities() result. The WordPress stubs declare the return
  as array, so PHPStan flagged the check as dead.
@webmyc

webmyc commented May 23, 2026

Copy link
Copy Markdown
Author

CI fixes pushed in fbc21e2:

  • tests/Unit/Admin/AbilityExposureFilterTest.php: parent declares set_up() public, so the override has to match visibility. Was firing fatals on every PHP / WP matrix combo.
  • includes/Plugin.php: is_admin() guard in setup() rewritten as an early return so SlevomatCodingStandard.ControlStructures.EarlyExit is happy at line 68.
  • includes/Admin/SettingsPage.php: dropped the redundant is_array() guard on wp_get_abilities() (stubs declare the return as array, so PHPStan was right).

Also a status update on the downstream reference plugin: respira-press/inhale-mcp-abilities is now live on the WordPress.org Plugin Directory at https://wordpress.org/plugins/inhale-mcp-abilities/ as of last week. Current release is v0.4.2 (up from v0.2.0 in my previous comment), and it still uses the canonical mcp_adapter_public_abilities option key proposed here, so once this PR lands the two surfaces read and write the same option without migration.

No rush from my side; happy to iterate further on scope, naming, or file structure if anything in this PR feels off.

@codecov

codecov Bot commented May 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 4.00000% with 144 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.85%. Comparing base (d0ac080) to head (0f8c8ae).
⚠️ Report is 4 commits behind head on trunk.

Files with missing lines Patch % Lines
includes/Admin/SettingsPage.php 0.00% 132 Missing ⚠️
includes/Admin/AbilityExposureFilter.php 42.85% 8 Missing ⚠️
includes/Plugin.php 0.00% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##              trunk     #184      +/-   ##
============================================
- Coverage     88.25%   84.85%   -3.41%     
- Complexity     1243     1284      +41     
============================================
  Files            54       55       +1     
  Lines          4035     4185     +150     
============================================
- Hits           3561     3551      -10     
- Misses          474      634     +160     
Flag Coverage Δ
unit 84.85% <4.00%> (-3.41%) ⬇️

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

☔ View full report in Codecov by Sentry.
📢 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.

WordPress.WP.Capabilities sniff only statically validates string literals.
Passing self::CAPABILITY produced "Couldn't determine the value" warnings
on add_options_page() and current_user_can() (line 47, line 156) which
turn the PHPCS step red on this PR's gate.

Inline the literal at both call sites and drop the now-unused constant.
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