Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
8d4a7ca
feat(annotations): Add support for MCP annotations on prompts and res…
galatanovidiu Nov 10, 2025
d9b7b73
feat(annotations): Add support for MCP annotations on tools
galatanovidiu Nov 10, 2025
1284837
docs: Update MCP annotations documentation to clarify type-specific b…
galatanovidiu Nov 10, 2025
ea1f25a
Add test fixtures for MCP annotation mapping
galatanovidiu Nov 10, 2025
1a53442
Update McpAdapterConfigTest for new annotation test fixtures
galatanovidiu Nov 10, 2025
e6c6f13
Update validator tests to use MCP-compliant annotation format
galatanovidiu Nov 10, 2025
84036d3
Add comprehensive MCP annotation validation tests for prompts
galatanovidiu Nov 10, 2025
a67eff3
Add comprehensive MCP annotation validation tests for resources
galatanovidiu Nov 10, 2025
1338f82
Add comprehensive MCP annotation validation tests for tools
galatanovidiu Nov 10, 2025
fd03768
Add tests for MCP annotation mapping in prompts
galatanovidiu Nov 10, 2025
4bd5906
Add tests for MCP annotation mapping in resources
galatanovidiu Nov 10, 2025
e406505
Add tests for MCP annotation mapping in tools
galatanovidiu Nov 10, 2025
410478f
Removes redundant field type checks in annotation processing
galatanovidiu Nov 10, 2025
5c62821
Refactors priority validation logic for clarity
galatanovidiu Nov 10, 2025
4279271
Update includes/Domain/Resources/RegisterAbilityAsMcpResource.php
galatanovidiu Nov 10, 2025
bdfd6f9
Update includes/Domain/Resources/McpResourceValidator.php
galatanovidiu Nov 10, 2025
94c7456
Update includes/Domain/Prompts/RegisterAbilityAsMcpPrompt.php
galatanovidiu Nov 10, 2025
00a6423
Add McpValidator utility class with shared validation methods
galatanovidiu Nov 10, 2025
26dff6f
Refactor ISO 8601 validation to use shared McpValidator utility
galatanovidiu Nov 10, 2025
baee55a
Update wp-env config to use WordPress master branch
galatanovidiu Nov 10, 2025
06b44eb
Extracts annotation mapping logic into shared utility class
galatanovidiu Nov 10, 2025
18f4bc4
Merge remote-tracking branch 'origin/fix/tool-annotations-mapping' in…
galatanovidiu Nov 10, 2025
e90ac38
Remove trailing whitespace in McpAnnotationMapper.php
galatanovidiu Nov 10, 2025
a324e79
Fix null annotation values being converted to false
galatanovidiu Nov 10, 2025
6c1452c
Remove unnecessary assertion in test_non_mcp_fields_are_filtered_out
galatanovidiu Nov 10, 2025
ea8a12c
test: strengthen invalid annotations test assertions
galatanovidiu Nov 10, 2025
fd72336
test: strengthen priority clamping assertions
galatanovidiu Nov 10, 2025
f7a961a
test: assert priority exists in clamping test
galatanovidiu Nov 10, 2025
578c6b7
test: strengthen invalid annotations filtering assertions
galatanovidiu Nov 10, 2025
b6bf7d7
test: adds test assertion for boolean type validation
galatanovidiu Nov 10, 2025
e5a8284
Adds explicit priority field validation check
galatanovidiu Nov 10, 2025
f0d3e7f
Update comment in McpAdapterConfigTest to clarify resource discovery …
galatanovidiu Nov 10, 2025
beac29c
Refactor priority field validation in McpAnnotationMapper
galatanovidiu Nov 10, 2025
e23f669
Add whitespace validation to base64 validator
galatanovidiu Nov 10, 2025
17b57ca
Add comprehensive test suite for McpValidator
galatanovidiu Nov 10, 2025
d3e00ee
Add comprehensive test suite for McpAnnotationMapper
galatanovidiu Nov 10, 2025
b3c3f4e
Refactor McpPromptValidator and McpResourceValidator to use centraliz…
galatanovidiu Nov 11, 2025
670e382
Add McpValidator utility class with shared validation methods
galatanovidiu Nov 11, 2025
19a9f97
Refactor McpPromptValidator to use shared McpValidator methods
galatanovidiu Nov 11, 2025
75bb8f4
Refactor McpResourceValidator to use shared McpValidator methods
galatanovidiu Nov 11, 2025
7f0519f
Refactor McpToolValidator to use shared McpValidator methods
galatanovidiu Nov 11, 2025
13e50e8
Update tests to use refactored validator methods
galatanovidiu Nov 11, 2025
5c639d3
Improve MCP annotation handling and validation
galatanovidiu Nov 11, 2025
44c49e6
Refactor RegisterAbilityAsMcpTool to streamline title assignment
galatanovidiu Nov 11, 2025
5964afc
Refactor ability metadata annotations for consistency
galatanovidiu Nov 11, 2025
94fec28
Add title assignment for annotations in RegisterAbilityAsMcpTool
galatanovidiu Nov 11, 2025
8e10f8b
Update ability tests to reflect annotation key changes
galatanovidiu Nov 11, 2025
0fa7819
Refactor validation logic in McpValidator and improve ability metadat…
galatanovidiu Nov 11, 2025
0b99a5b
Use null instead of empty string for ability_property in 1:1 mappings
galatanovidiu Nov 11, 2025
0bb6289
Use switch statement for annotation field validation
galatanovidiu Nov 11, 2025
b0df6c6
Use switch statements for annotation field validation
galatanovidiu Nov 11, 2025
08f972d
Simplify resolve_annotation_value return type
galatanovidiu Nov 11, 2025
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
2 changes: 1 addition & 1 deletion .wp-env.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://schemas.wp.org/trunk/wp-env.json",
"core": null,
"core": "WordPress/WordPress#master",
"plugins": ["WordPress/abilities-api", "./."],
"env": {
"development": {
Expand Down
187 changes: 142 additions & 45 deletions docs/guides/creating-abilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,32 +58,116 @@ wp_register_ability('my-plugin/my-ability', [

## MCP Annotations

Annotations provide behavior hints to MCP clients about how to handle your abilities. **All component types** (Tools, Resources, and Prompts) support annotations through the `meta.annotations` field:
Annotations provide behavior hints to MCP clients about how to handle your abilities. **Annotations are type-specific** - Tools use different annotations than Resources and Prompts.

### Annotation Format: WordPress Abilities API vs MCP

**Best Practice: Use WordPress Abilities API Format**

The MCP Adapter automatically converts WordPress Abilities API annotation names to MCP format. **It's recommended to use the WordPress Abilities API format** when available for consistency across the WordPress ecosystem.

#### For Tools: WordPress Format Preferred

```php
// ✅ RECOMMENDED: WordPress Abilities API format
'meta' => [
'annotations' => [
'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
'title' => 'My Tool' // No WordPress equivalent, use MCP format
]
]

// ✅ ALSO VALID: Direct MCP format
'meta' => [
'annotations' => [
'readOnlyHint' => true,
'destructiveHint' => false,
'idempotentHint' => true,
'openWorldHint' => false,
'title' => 'My Tool'
]
]
```

**Tool Annotation Mapping Table:**

| WordPress Format | MCP Format | Description |
|-----------------|------------|-------------|
| `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 |
| *(no equivalent)* | `title` | Custom display title |

**Why Use WordPress Format?**
- **Consistency**: Matches WordPress Abilities API conventions
- **Familiarity**: WordPress developers already know these terms
- **Future-proof**: Additional WordPress formats may be added
- **Interoperability**: Works with other WordPress Abilities API consumers

#### For Resources & Prompts: MCP Format Only

Resources and Prompts use MCP format directly - there are no WordPress equivalents:

```php
'meta' => [
'annotations' => [
'audience' => ['user', 'assistant'], // MCP format (no WordPress equivalent)
'lastModified' => '2024-01-15T10:30:00Z', // MCP format (no WordPress equivalent)
'priority' => 0.8 // MCP format (no WordPress equivalent)
]
]
```

### Tool Annotations (ToolAnnotations)

Tools support these MCP specification annotations:

```php
'meta' => [
'annotations' => [
'priority' => 1.0, // Execution priority (higher = more important)
'readOnlyHint' => true, // Component doesn't modify data
'destructiveHint' => false, // Component doesn't delete/destroy data
'idempotentHint' => true, // Same input always produces same output
'openWorldHint' => false, // Component works with predefined data only
'readOnlyHint' => true, // Tool doesn't modify data
'destructiveHint' => false, // Tool doesn't delete/destroy data
'idempotentHint' => true, // Same input → same output
'openWorldHint' => false, // Works with predefined data only
'title' => 'Custom Title' // Display title (optional)
]
]
```

### Standard MCP Annotations
**Supported Tool Annotation Fields:**
- `readOnlyHint` (bool): Tool doesn't modify data
- `destructiveHint` (bool): Tool may delete or destroy data
- `idempotentHint` (bool): Same input always produces same output
- `openWorldHint` (bool): Tool can work with arbitrary/unknown data
- `title` (string): Custom display title for the tool

**WordPress → MCP Field Conversion**: For backward compatibility, Tools support WordPress-format field names that are automatically converted:
- `readonly` → `readOnlyHint`
- `destructive` → `destructiveHint`
- `idempotent` → `idempotentHint`

### Resource & Prompt Annotations (Annotations)

Resources and Prompts share the same annotation schema per MCP specification:

**Universal Annotations** (supported by all component types):
- `priority` (float): Execution priority (default: 1.0, higher = more important)
- `readOnlyHint` (bool): Indicates read-only operations
- `destructiveHint` (bool): Warns about destructive operations
- `idempotentHint` (bool): Same input produces same output
- `openWorldHint` (bool): Can work with arbitrary/unknown data
```php
'meta' => [
'annotations' => [
'audience' => ['user', 'assistant'], // Intended audience
'lastModified' => '2024-01-15T10:30:00Z', // ISO 8601 timestamp
'priority' => 0.8 // 0.0 (lowest) to 1.0 (highest)
]
]
```

**Resource-Specific Annotations** (as per MCP specification):
- `audience` (array): Intended audience (`["user", "assistant"]`)
**Supported Resource & Prompt Annotation Fields:**
- `audience` (array): Intended roles - `["user"]`, `["assistant"]`, or both
- `lastModified` (string): ISO 8601 timestamp of last modification
- `priority` (float): Relative importance (0.0 = lowest, 1.0 = highest)

### Annotation Usage by Component Type

Expand All @@ -94,7 +178,7 @@ Annotations provide behavior hints to MCP clients about how to handle your abili
### Complete Annotation Example

```php
// Tool with comprehensive annotations
// Tool with WordPress Abilities API format (RECOMMENDED)
wp_register_ability('my-plugin/analyze-data', [
'label' => 'Data Analyzer',
'description' => 'Analyze data with various algorithms',
Expand All @@ -103,16 +187,20 @@ wp_register_ability('my-plugin/analyze-data', [
'permission_callback' => function() { return current_user_can('read'); },
'meta' => [
'annotations' => [
'priority' => 2.0, // High priority
'readOnlyHint' => true, // Read-only operation
'destructiveHint' => false, // Safe operation
'idempotentHint' => true, // Consistent results
'openWorldHint' => false // Works with known data
'readonly' => true, // WordPress format → readOnlyHint
'destructive' => false, // WordPress format → destructiveHint
'idempotent' => true, // WordPress format → idempotentHint
'openWorldHint' => false, // No WordPress equivalent
'title' => 'Data Analysis Tool' // No WordPress equivalent
],
'mcp' => [
'public' => true,
'type' => 'tool'
]
]
]);

// Resource with MCP-specific annotations
// Resource with Resource-specific annotations
wp_register_ability('my-plugin/user-data', [
'label' => 'User Data Resource',
'description' => 'Access to user profile data',
Expand All @@ -123,27 +211,37 @@ wp_register_ability('my-plugin/user-data', [
'annotations' => [
'audience' => ['assistant'], // For AI use only
'priority' => 0.9, // High importance
'lastModified' => date('c'), // ISO 8601 timestamp
'readOnlyHint' => true
'lastModified' => date('c') // ISO 8601 timestamp
],
'mcp' => [
'public' => true,
'type' => 'resource'
]
]
]);

// Prompt with behavior annotations
// Prompt with Prompt-specific annotations
wp_register_ability('my-plugin/review-prompt', [
'label' => 'Code Review Prompt',
'description' => 'Generate structured code review prompts',
'input_schema' => [
'type' => 'object',
'properties' => [
'code' => ['type' => 'string', 'description' => 'Code to review']
],
'required' => ['code']
],
'execute_callback' => 'generate_review_prompt',
'permission_callback' => function() { return current_user_can('edit_posts'); },
'meta' => [
'arguments' => [
['name' => 'code', 'description' => 'Code to review', 'required' => true]
],
'annotations' => [
'priority' => 1.5, // Above average priority
'readOnlyHint' => true, // Doesn't modify data
'idempotentHint' => true, // Consistent output
'openWorldHint' => true // Can handle any code
'audience' => ['user', 'assistant'], // For both user and AI
'priority' => 0.8, // High priority
'lastModified' => date('c') // Current timestamp
],
'mcp' => [
'public' => true,
'type' => 'prompt'
]
]
]);
Expand Down Expand Up @@ -202,9 +300,9 @@ wp_register_ability('my-plugin/create-post', [
},
'meta' => [
'annotations' => [
'priority' => 2.0,
'readOnlyHint' => false,
'destructiveHint' => false
'readonly' => false, // Tool modifies data (WordPress format)
'destructive' => false, // Tool doesn't delete data (WordPress format)
'idempotent' => false // Multiple calls create multiple posts (WordPress format)
],
'mcp' => [
'public' => true // Expose this ability via MCP
Expand Down Expand Up @@ -236,11 +334,9 @@ wp_register_ability('my-plugin/site-config', [
'meta' => [
'uri' => 'wordpress://site/config',
'annotations' => [
'readOnlyHint' => true,
'idempotentHint' => true,
'audience' => ['user', 'assistant'],
'priority' => 0.8,
'lastModified' => '2024-01-15T10:30:00Z'
'audience' => ['user', 'assistant'], // For both users and AI
'priority' => 0.8, // High priority resource
'lastModified' => '2024-01-15T10:30:00Z' // Last update timestamp
],
'mcp' => [
'public' => true, // Expose this ability via MCP
Expand Down Expand Up @@ -317,8 +413,8 @@ wp_register_ability('my-plugin/code-review', [
},
'meta' => [
'annotations' => [
'readOnlyHint' => true, // Template doesn't modify data
'idempotentHint' => true // Consistent prompt generation
'audience' => ['user'], // For user-facing prompts
'priority' => 0.7 // Standard priority
],
'mcp' => [
'public' => true, // Expose this ability via MCP
Expand Down Expand Up @@ -382,8 +478,9 @@ wp_register_ability('my-plugin/analysis-prompt', [
},
'meta' => [
'annotations' => [
'readOnlyHint' => true,
'openWorldHint' => true // Can handle any data type
'audience' => ['assistant'], // For AI analysis only
'priority' => 0.9, // High priority analysis
'lastModified' => date('c') // Current timestamp
],
'mcp' => [
'public' => true, // Expose this ability via MCP
Expand All @@ -398,7 +495,7 @@ wp_register_ability('my-plugin/analysis-prompt', [
**Template-Level Annotations** (in `meta.annotations`):
- Apply to the prompt template itself
- Describe the prompt's behavior characteristics
- Support all standard MCP annotations (readOnlyHint, idempotentHint, etc.)
- Support Prompt-specific annotations: `audience`, `priority`, `lastModified`

**Message Content Annotations** (in message `content.annotations`):
- Apply to individual messages within the prompt
Expand Down
8 changes: 3 additions & 5 deletions includes/Abilities/DiscoverAbilitiesAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,9 @@ public static function register(): void {
'execute_callback' => array( self::class, 'execute' ),
'meta' => array(
'annotations' => array(
'priority' => '1.0',
'readOnlyHint' => true,
'destructiveHint' => false,
'idempotentHint' => true,
'openWorldHint' => false,
'readonly' => true,
'destructive' => false,
'idempotent' => true,
),
),
)
Expand Down
6 changes: 3 additions & 3 deletions includes/Abilities/ExecuteAbilityAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ public static function register(): void {
'execute_callback' => array( self::class, 'execute' ),
'meta' => array(
'annotations' => array(
'priority' => '1.0',
'readOnlyHint' => false,
'openWorldHint' => true,
'readonly' => false,
'destructive' => true,
'idempotent' => false,
),
),
)
Expand Down
8 changes: 3 additions & 5 deletions includes/Abilities/GetAbilityInfoAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,9 @@ public static function register(): void {
'execute_callback' => array( self::class, 'execute' ),
'meta' => array(
'annotations' => array(
'priority' => '1.0',
'readOnlyHint' => true,
'destructiveHint' => false,
'idempotentHint' => true,
'openWorldHint' => false,
'readonly' => true,
'destructive' => false,
'idempotent' => true,
),
),
)
Expand Down
15 changes: 13 additions & 2 deletions includes/Domain/Prompts/McpPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ class McpPrompt {
/**
* The MCP server instance this prompt belongs to.
*
* @var \WP\MCP\Core\McpServer
* @var \WP\MCP\Core\McpServer|null
*/
private McpServer $mcp_server;
private ?McpServer $mcp_server = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused why it's possible for the McpServer to be nullable. Is there a use case for this? Or should it be a required parameter in the constructor?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question for Tool and Resource.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows flexible component registration, components can be created independently and then associated with a server during registration.

I can modify this and make it more strict if you prefer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. This actually raises a different question, then: Why are the prompts, tools, and resources coupled to a specific server instance, rather than something ingested by the server?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hhhh, yes, this creates a circular dependency:

  • Components depend on the server (via get_mcp_server())
  • The server contains/registers components

Probably, we can check for uniqueness and validate it inside McpComponentRegistry (or something similar). I need to think about this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I would think the dependency chain is Server > Registry > Tools/Resources/Prompts. Ideally, the dependency is uni-directional, so the components aren't aware of the registry or server, and the registry isn't aware of the server.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we address this on another issue/PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let's resolve this in a subsequent PR. 👍


/**
* Constructor for McpPrompt.
Expand Down Expand Up @@ -356,6 +356,13 @@ public static function from_array( array $data, McpServer $mcp_server ) {
* @return self|\WP_Error Returns the validated prompt instance or WP_Error if validation fails.
*/
public function validate( string $context = '' ) {
if ( null === $this->mcp_server ) {
return new \WP_Error(
'prompt_missing_mcp_server',
esc_html__( 'MCP server must be set before validating a prompt.', 'mcp-adapter' )
);
}

if ( ! $this->mcp_server->is_mcp_validation_enabled() ) {
return $this;
}
Expand Down Expand Up @@ -401,6 +408,10 @@ public static function create_argument( string $name, ?string $description = nul
* @return \WP\MCP\Core\McpServer
*/
public function get_mcp_server(): McpServer {
if ( null === $this->mcp_server ) {
throw new \RuntimeException( 'MCP server has not been set on this prompt instance.' );
}

return $this->mcp_server;
}

Expand Down
Loading
Loading