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 7 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 7 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.

@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
Comment thread includes/Admin/SettingsPage.php Outdated
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
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: 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.
@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 (90252e7) to head (4bd9e73).

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     87.86%   84.85%   -3.01%     
- Complexity     1245     1286      +41     
============================================
  Files            53       55       +2     
  Lines          4037     4187     +150     
============================================
+ Hits           3547     3553       +6     
- Misses          490      634     +144     
Flag Coverage Δ
unit 84.85% <4.00%> (-3.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.
webmyc and others added 7 commits June 28, 2026 19:10
…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>
- 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>
- 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.
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.
Adds unit coverage for the settings page save path and ability discovery,
the security-critical part of this PR:

- sanitize_option rejects non-arrays, drops unregistered ability names,
  drops the mcp-adapter/ managed namespace even when registered, drops
  empty and non-string entries, deduplicates, and preserves input order
- discover_abilities keys abilities by name, flags the managed namespace
  the UI disables, and returns an alphabetically sorted list

These cover the opt-in path that decides which abilities reach the default
MCP server, which previously had no direct test coverage.
…ge gate

The settings page is mostly wp-admin render markup, exercised end to end
rather than unit covered. Allow a small project dip (threshold 5%) and treat
the diff/patch coverage as informational, so a UI-heavy change reports
coverage without failing the PR.
Extends the SettingsPage tests beyond sanitize_option/discover_abilities to
exercise register(), register_setting(), and render_page() (the abilities
table with option-bound checkboxes, the checked state, and the disabled
managed-namespace row).
@webmyc
webmyc force-pushed the feat/issue-183-default-ability-opt-in-ui branch from 04d1dec to 4bd9e73 Compare June 28, 2026 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants