Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/guides/creating-abilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ The MCP Adapter automatically converts WordPress Abilities API annotation names
'readonly' => true, // Auto-converted to readOnlyHint
'destructive' => false, // Auto-converted to destructiveHint
'idempotent' => true, // Auto-converted to idempotentHint
'openWorldHint' => false, // No WordPress equivalent, use MCP format
'open_world' => false, // Auto-converted to openWorldHint
'title' => 'My Tool' // No WordPress equivalent, use MCP format
]
]
Expand All @@ -336,7 +336,7 @@ The MCP Adapter automatically converts WordPress Abilities API annotation names
| `readonly` | `readOnlyHint` | Tool doesn't modify data |
| `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.

| *(no equivalent)* | `title` | Custom display title |

**Why Use WordPress Format?**
Expand Down Expand Up @@ -386,6 +386,7 @@ Tools support these MCP specification annotations:
- `readonly` → `readOnlyHint`
- `destructive` → `destructiveHint`
- `idempotent` → `idempotentHint`
- `open_world` → `openWorldHint`

### Resource & Prompt Annotations (Annotations)

Expand Down Expand Up @@ -427,7 +428,7 @@ wp_register_ability('my-plugin/analyze-data', [
'readonly' => true, // WordPress format → readOnlyHint
'destructive' => false, // WordPress format → destructiveHint
'idempotent' => true, // WordPress format → idempotentHint
'openWorldHint' => false, // No WordPress equivalent
'open_world' => false, // WordPress format → openWorldHint
'title' => 'Data Analysis Tool' // No WordPress equivalent
],
'mcp' => [
Expand Down
2 changes: 1 addition & 1 deletion includes/Domain/Utils/McpAnnotationMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class McpAnnotationMapper {
'openWorldHint' => array(
'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.

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.

),
'title' => array(
'type' => 'string',
Expand Down
30 changes: 26 additions & 4 deletions tests/phpunit/Unit/Domain/Utils/McpAnnotationMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,30 @@ public function test_map_maps_idempotent_to_idempotenthint(): void {
$this->assertTrue( $result['idempotentHint'] );
}

public function test_map_maps_open_world_to_openworldhint(): void {
$annotations = array(
'open_world' => false,
);

$result = McpAnnotationMapper::map( $annotations, 'tool' );

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


public function test_open_world_override_takes_precedence_over_openworldhint(): void {
$annotations = array(
'openWorldHint' => true,
'open_world' => false,
);

$result = McpAnnotationMapper::map( $annotations, 'tool' );

$this->assertArrayHasKey( 'openWorldHint', $result );
$this->assertFalse( $result['openWorldHint'], 'WordPress-format open_world should override openWorldHint value' );
}

public function test_map_excludes_tool_fields_for_resource(): void {
$annotations = array(
'readonly' => true,
Expand Down Expand Up @@ -232,15 +256,13 @@ public function test_map_performs_light_type_validation_for_tools(): void {
public function test_map_with_null_ability_property_uses_mcp_field_name_for_tools(): void {
$annotations = array(
// Fields with null ability_property should map 1:1.
// For tools, only openWorldHint and title have null ability_property.
'openWorldHint' => true,
'title' => 'Test',
// For tools, only title has null ability_property.
'title' => 'Test',
);

$result = McpAnnotationMapper::map( $annotations, 'tool' );

// These should map 1:1 (ability_property is null)
$this->assertArrayHasKey( 'openWorldHint', $result );
$this->assertArrayHasKey( 'title', $result );
}

Expand Down
Loading