feat(admin): add Settings > MCP Adapter page to opt registered abilities into the default server (fixes #183)#184
Conversation
…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>
|
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. |
There was a problem hiding this comment.
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_abilitiesoption. - Adds a
wp_register_ability_argsfilter that reads the saved option and injectsmeta.mcp.public = truefor 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.
| </thead> | ||
| <tbody> | ||
| <?php foreach ( $abilities as $name => $info ) : ?> | ||
| <tr<?php echo $info['managed'] ? ' class="disabled"' : ''; ?>> |
There was a problem hiding this comment.
Good catch. Computed the value into a $row_class string and echoed it through esc_attr(). Pushed in 8cb5be2.
| */ | ||
| 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' ) ); |
There was a problem hiding this comment.
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.
- 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>
- 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.
e8a7fcd to
41f12a6
Compare
- 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>
41f12a6 to
017ab9c
Compare
- 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>
017ab9c to
8cb5be2
Compare
|
Copilot review feedback is in (commit 8cb5be2):
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 No rush on review; happy to iterate on scope, naming or file structure. |
…, 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.
|
CI fixes pushed in fbc21e2:
Also a status update on the downstream reference plugin: No rush from my side; happy to iterate further on scope, naming, or file structure if anything in this PR feels off. |
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
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.
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-mcpshipped 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_argsfilter documented on WordPress contributor blogs and several developer guides. Third-party helper plugins exist but are inconsistent.What this PR adds
manage_options.mcp_adapter_public_abilities.wp_register_ability_argsfilter that applies the saved selection at registration time. Same hook the documented workaround uses, so existing developer code is unaffected.tests/Unit/Admin/AbilityExposureFilterTest.php).README.mdunder "Basic Usage".What this PR does not add
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
declare(strict_types=1);.Manual test plan
mcp-adapteron a WordPress 7.0 site.get_option( 'mcp_adapter_public_abilities' )returns those abilities.wp_get_ability( 'core/get-site-info' )->get_meta()(or equivalent) returnsmeta.mcp.public = truefor the selected ones.npm run lint:php,npm run lint:php:stan,npm run test:php. All green.Follow-ups suggested for separate PRs
@wordpress/scripts.Notes for reviewers
This is a first contribution to
mcp-adapter. Happy to iterate on scope, naming, or implementation patterns. The standalonerespira-press/inhale-mcp-abilitiesplugin exists to keep momentum during review while still demonstrating the architecture.