feat: map open_world ability annotation to openWorldHint - #231
feat: map open_world ability annotation to openWorldHint#231jorgefilipecosta wants to merge 1 commit into
Conversation
|
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 support for the WordPress Abilities API open_world annotation by mapping it to the MCP tool annotation openWorldHint, ensuring “closed-world” abilities aren’t accidentally treated as open-world by MCP clients when the hint is omitted.
Changes:
- Map
openWorldHint’s WordPress-format equivalent toopen_worldinMcpAnnotationMapper. - Add/adjust unit tests to cover the new mapping and confirm WordPress-format precedence over MCP-format keys.
- Update documentation to reflect the new
open_world→openWorldHintconversion.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| tests/phpunit/Unit/Domain/Utils/McpAnnotationMapperTest.php | Adds tests for open_world mapping and precedence; updates expectations for null ability_property cases. |
| includes/Domain/Utils/McpAnnotationMapper.php | Implements the open_world → openWorldHint mapping via ability_property. |
| docs/guides/creating-abilities.md | Documents the new WordPress-format key and updates examples/mapping table accordingly. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## trunk #231 +/- ##
=========================================
Coverage 87.86% 87.86%
Complexity 1245 1245
=========================================
Files 53 53
Lines 4037 4037
=========================================
Hits 3547 3547
Misses 490 490
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:
|
| | `destructive` | `destructiveHint` | Tool may delete/destroy data | | ||
| | `idempotent` | `idempotentHint` | Same input → same output | | ||
| | *(no equivalent)* | `openWorldHint` | Can work with arbitrary data | | ||
| | `open_world` | `openWorldHint` | Can work with arbitrary data | |
There was a problem hiding this comment.
question: small naming one. The other three WordPress-format hints are single words (readonly, destructive, idempotent), so by that pattern this key would read openworld. I'd lean open_world anyway since snake_case is more WordPress, but since it's becoming an Abilities API convention, could we settle the spelling on purpose so the adapter and the Abilities API stay in sync?
There was a problem hiding this comment.
, but since it's becoming an Abilities API convention,
@galatanovidiu what do you mean by this part? "openWorldHint" seems very mcp-specific. IMO even if we bring support for marking Abilities that handle external data, I would assume we choose something that's semantic and self-defining to WordPress and then have MCP Adapter map it to here.
Regardless of the Abilities part of this, +1 to open_world, and consider readonly an exception-to-the-rule, instead of establishing a future pattern of stripping multi-word semantics when going from camelCase.
| $this->assertArrayHasKey( 'openWorldHint', $result ); | ||
| $this->assertArrayNotHasKey( 'open_world', $result ); | ||
| $this->assertFalse( $result['openWorldHint'] ); | ||
| } |
There was a problem hiding this comment.
open_world itself is never exercised on the feature-gating or invalid-boolean paths. Could we add 'open_world' => false to the resource and prompt exclusion tests with an assertArrayNotHasKey( 'openWorldHint', $result ), plus one non-boolean open_world case that gets dropped? That locks the new key in against a future regression.
| 'type' => 'boolean', | ||
| 'features' => array( 'tool' ), | ||
| 'ability_property' => null, | ||
| 'ability_property' => 'open_world', |
There was a problem hiding this comment.
One edge from replaying old registrations: ['open_world' => 'invalid', 'openWorldHint' => false] used to emit openWorldHint => false, now it emits nothing, and the spec default for a missing hint is true. It comes from the shared resolution path: resolve_annotation_value() picks the WP-format value whenever the key exists, and when normalize_boolean() rejects it the whole hint is dropped instead of falling back to the still-valid MCP key. The same edge already exists for the other three hints, so nothing to change here. I'll open an issue to add the fallback for all four.
| 'type' => 'boolean', | ||
| 'features' => array( 'tool' ), | ||
| 'ability_property' => null, | ||
| 'ability_property' => 'open_world', |
There was a problem hiding this comment.
nit: could we add @since n.e.x.t for the new mapping in the $mcp_annotations or map() docblock? The file has no @since tags anywhere, so nothing records that open_world support arrives in the next release.
There was a problem hiding this comment.
Handy addition: open_world now works as the WordPress-format alias for openWorldHint, like the other three hints. Before this you had to set the MCP-format openWorldHint directly (which the docs said to do), and a WordPress-format open_world key was silently dropped, so an ability like core/read-content in WordPress/ai#739 that wrote open_world => false lost its closed-world hint and fell back to the spec default of true. Nothing blocking. Worth a look, left inline:
- naming: by the one-word pattern of the other hints this would read
openworld; worth settling the spelling on purpose since it's an Abilities API convention. - no test exercises the tool-only feature gate or the invalid-value branch through the
open_worldkey itself. - one replay edge: an invalid
open_worldnext to a validopenWorldHintnow drops the hint. Shared resolution path (pre-exists for the other three hints), so nothing for this PR; I'll open a separate issue.
PHPCS, PHPStan L8, and the mapper tests (24/24) pass locally. Registrations using only openWorldHint replay byte-identical.
Reviewed by Claude and Codex, checked by me.
|
(rest of this lgtm once the open comments are handled) |
What?
Follow up to WordPress/ai#739, which introduces the WordPress-format
open_worldannotation on thecore/read-contentability.Adds
open_worldto the WordPress → MCP annotation conversion inMcpAnnotationMapper, so abilities can declare the MCPopenWorldHinttool annotation using the WordPress Abilities API naming convention, like the existingreadonly,destructive, andidempotentkeys.Why?
The adapter already converts WordPress-format annotation keys to MCP tool annotations (
readonly→readOnlyHint,destructive→destructiveHint,idempotent→idempotentHint), butopenWorldHinthad no WordPress-format equivalent (ability_property => null), so a WordPress-format'open_world' => falseannotation was silently dropped.Per the MCP specification, clients assume
openWorldHintistruewhen the hint is absent. That means closed-world abilities that declare'open_world' => false— such ascore/read-contentin WordPress/ai#739, which only reads the local database — were presented to MCP clients as tools that may interact with external systems.How?
ability_propertyforopenWorldHinttoopen_worldinMcpAnnotationMapper::$mcp_annotations. The existing resolution logic handles the rest: the WordPress-format key takes precedence when both are present, and the direct MCP-formatopenWorldHintkey keeps working unchanged.ability_propertytest (for tools, onlytitleremains without a WordPress equivalent).docs/guides/creating-abilities.md.Testing Instructions
'meta' => array( 'annotations' => array( 'open_world' => false ) )and expose it as a tool on an MCP server.tools/liston the server and confirm the tool's annotations include"openWorldHint": false(before this change the hint was missing).Or run the mapper unit tests:
Changelog Entry