Skip to content

feat: map open_world ability annotation to openWorldHint - #231

Open
jorgefilipecosta wants to merge 1 commit into
trunkfrom
feat/open-world-annotation-mapping
Open

feat: map open_world ability annotation to openWorldHint#231
jorgefilipecosta wants to merge 1 commit into
trunkfrom
feat/open-world-annotation-mapping

Conversation

@jorgefilipecosta

Copy link
Copy Markdown
Member

What?

Follow up to WordPress/ai#739, which introduces the WordPress-format open_world annotation on the core/read-content ability.

Adds open_world to the WordPress → MCP annotation conversion in McpAnnotationMapper, so abilities can declare the MCP openWorldHint tool annotation using the WordPress Abilities API naming convention, like the existing readonly, destructive, and idempotent keys.

Why?

The adapter already converts WordPress-format annotation keys to MCP tool annotations (readonlyreadOnlyHint, destructivedestructiveHint, idempotentidempotentHint), but openWorldHint had no WordPress-format equivalent (ability_property => null), so a WordPress-format 'open_world' => false annotation was silently dropped.

Per the MCP specification, clients assume openWorldHint is true when the hint is absent. That means closed-world abilities that declare 'open_world' => false — such as core/read-content in WordPress/ai#739, which only reads the local database — were presented to MCP clients as tools that may interact with external systems.

How?

  • Sets the ability_property for openWorldHint to open_world in McpAnnotationMapper::$mcp_annotations. The existing resolution logic handles the rest: the WordPress-format key takes precedence when both are present, and the direct MCP-format openWorldHint key keeps working unchanged.
  • Adds unit tests for the new mapping and its precedence over the MCP-format key, and updates the null-ability_property test (for tools, only title remains without a WordPress equivalent).
  • Documents the new mapping in docs/guides/creating-abilities.md.

Testing Instructions

  1. Register an ability with 'meta' => array( 'annotations' => array( 'open_world' => false ) ) and expose it as a tool on an MCP server.
  2. Call tools/list on the server and confirm the tool's annotations include "openWorldHint": false (before this change the hint was missing).

Or run the mapper unit tests:

npm run test:php -- --filter McpAnnotationMapperTest

Changelog Entry

Added - Support the WordPress-format open_world ability annotation, mapped to the MCP openWorldHint tool annotation.

Copilot AI review requested due to automatic review settings July 3, 2026 15:23
@github-actions

github-actions Bot commented Jul 3, 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: jorgefilipecosta <jorgefilipecosta@git.wordpress.org>
Co-authored-by: galatanovidiu <ovidiu-galatan@git.wordpress.org>
Co-authored-by: justlevine <justlevine@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 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 to open_world in McpAnnotationMapper.
  • 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_worldopenWorldHint conversion.

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

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.86%. Comparing base (90252e7) to head (7392deb).

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           
Flag Coverage Δ
unit 87.86% <ø> (ø)

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.

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

@galatanovidiu galatanovidiu Jul 7, 2026

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.

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?

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.

, 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'] );
}

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.

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',

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.

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',

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.

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.

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

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_world key itself.
  • one replay edge: an invalid open_world next to a valid openWorldHint now 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.

@justlevine

Copy link
Copy Markdown
Contributor

(rest of this lgtm once the open comments are handled)

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.

4 participants