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
53 changes: 50 additions & 3 deletions includes/Domain/Prompts/RegisterAbilityAsMcpPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace WP\MCP\Domain\Prompts;

use WP\MCP\Domain\Utils\McpNameSanitizer;
use WP\MCP\Domain\Utils\McpValidator;
use WP\MCP\Domain\Utils\SchemaTransformer;
use WP\McpSchema\Server\Prompts\DTO\Prompt as PromptDto;
Expand Down Expand Up @@ -211,9 +212,13 @@ private function build_prompt_data() {
*
*/
private function get_data() {
$ability_name = trim( $this->ability->get_name() );
$prompt_data = array(
'name' => str_replace( '/', '-', $ability_name ),
$prompt_name = $this->resolve_prompt_name();
if ( is_wp_error( $prompt_name ) ) {
return $prompt_name;
}

$prompt_data = array(
'name' => $prompt_name,
);

// Add optional title from ability label.
Expand Down Expand Up @@ -427,6 +432,48 @@ private function convert_input_schema_to_arguments( array $input_schema ): array
return $arguments;
}

/**
* Resolve the MCP prompt name from ability.
*
* Sanitizes the ability name to MCP-valid format, applies filter, and validates result.
*
* @since n.e.x.t
*
* @return string|\WP_Error Valid prompt name or error.
*/
private function resolve_prompt_name() {
// Sanitize ability name to MCP-valid format.
$sanitized_name = McpNameSanitizer::sanitize_name( $this->ability->get_name() );

if ( is_wp_error( $sanitized_name ) ) {
return $sanitized_name;
}

/**
* Filters the MCP prompt name derived from an ability.
*
* @since n.e.x.t
*
* @param string $name The sanitized prompt name.
* @param \WP_Ability $ability The source ability instance.
*/
$filtered_name = apply_filters( 'mcp_adapter_prompt_name', $sanitized_name, $this->ability );

// Validate post-filter (in case filter broke it).
if ( ! is_string( $filtered_name ) || ! McpValidator::validate_name( $filtered_name ) ) {
return new WP_Error(
'mcp_prompt_name_filter_invalid',
sprintf(
/* translators: %s: invalid prompt name returned by filter */
__( 'Filter returned invalid MCP prompt name: %s', 'mcp-adapter' ),
is_string( $filtered_name ) ? $filtered_name : gettype( $filtered_name )
)
);
}

return $filtered_name;
}

/**
* Build a clean Prompt DTO and adapter metadata for internal wiring.
*
Expand Down
17 changes: 16 additions & 1 deletion includes/Handlers/Initialize/InitializeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,28 @@ public function handle( string $client_protocol_version ): InitializeResult {
)
);

return InitializeResult::fromArray(
$result = InitializeResult::fromArray(
array(
'protocolVersion' => $negotiated_version,
'capabilities' => $capabilities,
'serverInfo' => $server_info,
'instructions' => $this->mcp->get_server_description(),
)
);

/**
* Filters the initialize response before returning to the client.
*
* Use this filter to modify server capabilities, instructions, or
* other initialization data dynamically. To modify the result, call
* `$result->toArray()`, change the data, and return
* `InitializeResult::fromArray( $modified_data )`.
*
* @since n.e.x.t
*
* @param \WP\McpSchema\Common\Protocol\DTO\InitializeResult $result The initialize result DTO.
* @param \WP\MCP\Core\McpServer $server The MCP server instance.
*/
return apply_filters( 'mcp_adapter_initialize_response', $result, $this->mcp );
}
}
50 changes: 50 additions & 0 deletions includes/Handlers/Prompts/PromptsHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ public function __construct( McpServer $mcp ) {
public function list_prompts(): ListPromptsResult {
$prompts = array_values( $this->mcp->get_prompts() );

/**
* Filters the list of prompts before returning to the client.
*
* Use this filter to filter prompts by context, add dynamic prompts,
* or reorder the prompts list.
*
* @since n.e.x.t
*
* @param array<\WP\McpSchema\Server\Prompts\DTO\Prompt> $prompts Array of Prompt DTOs.
* @param \WP\MCP\Core\McpServer $server The MCP server instance.
*/
$prompts = apply_filters( 'mcp_adapter_prompts_list', $prompts, $this->mcp );

return ListPromptsResult::fromArray(
array(
'prompts' => $prompts,
Expand Down Expand Up @@ -119,7 +132,44 @@ public function get_prompt( array $params, $request_id = 0 ) {
return McpErrorFactory::permission_denied( $request_id, $error_message );
}

/**
* Filters prompt arguments before execution, or short-circuits execution entirely.
*
* Return the (optionally modified) arguments array to proceed with execution,
* or return a WP_Error to block execution and return an error to the client.
*
* @since n.e.x.t
*
* @param array $arguments The prompt arguments.
* @param string $prompt_name The prompt name being retrieved.
* @param \WP\MCP\Domain\Prompts\McpPrompt $mcp_prompt The MCP prompt instance.
* @param \WP\MCP\Core\McpServer $server The MCP server instance.
*/
$arguments = apply_filters( 'mcp_adapter_pre_prompt_get', $arguments, $prompt_name, $mcp_prompt, $this->mcp );

// Allow pre-filter to short-circuit execution by returning WP_Error.
if ( is_wp_error( $arguments ) ) {
return McpErrorFactory::permission_denied( $request_id, $arguments->get_error_message() );
}

$result = $mcp_prompt->execute( $arguments );

/**
* Filters the prompt execution result before normalization.
*
* Use this filter for message transformation, context injection,
* content enrichment, or audit logging.
*
* @since n.e.x.t
*
* @param mixed $result The raw execution result.
* @param array $arguments The prompt arguments used.
* @param string $prompt_name The prompt name.
* @param \WP\MCP\Domain\Prompts\McpPrompt $mcp_prompt The MCP prompt instance.
* @param \WP\MCP\Core\McpServer $server The MCP server instance.
*/
$result = apply_filters( 'mcp_adapter_prompt_get_result', $result, $arguments, $prompt_name, $mcp_prompt, $this->mcp );

if ( is_wp_error( $result ) ) {
$this->mcp->error_handler->log(
'Prompt execution returned WP_Error',
Expand Down
49 changes: 49 additions & 0 deletions includes/Handlers/Resources/ResourcesHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ public function __construct( McpServer $mcp ) {
public function list_resources(): ListResourcesResult {
$resources = array_values( $this->mcp->get_resources() );

/**
* Filters the list of resources before returning to the client.
*
* Use this filter to filter resources by context, add dynamic resources,
* or reorder the resources list.
*
* @since n.e.x.t
*
* @param array<\WP\McpSchema\Server\Resources\DTO\Resource> $resources Array of Resource DTOs.
* @param \WP\MCP\Core\McpServer $server The MCP server instance.
*/
$resources = apply_filters( 'mcp_adapter_resources_list', $resources, $this->mcp );

return ListResourcesResult::fromArray(
array(
'resources' => $resources,
Expand Down Expand Up @@ -104,8 +117,44 @@ public function read_resource( array $params, $request_id = 0 ) {
return McpErrorFactory::permission_denied( $request_id, $error_message );
}

/**
* Filters resource parameters before execution, or short-circuits execution entirely.
*
* Return the (optionally modified) parameters array to proceed with execution,
* or return a WP_Error to block execution and return an error to the client.
*
* @since n.e.x.t
*
* @param array $params The request parameters.
* @param string $uri The resource URI.
* @param \WP\MCP\Domain\Resources\McpResource $mcp_resource The MCP resource instance.
* @param \WP\MCP\Core\McpServer $server The MCP server instance.
*/
$request_params = apply_filters( 'mcp_adapter_pre_resource_read', $request_params, $uri, $mcp_resource, $this->mcp );

// Allow pre-filter to short-circuit execution by returning WP_Error.
if ( is_wp_error( $request_params ) ) {
return McpErrorFactory::internal_error( $request_id, $request_params->get_error_message() );
}

$contents = $mcp_resource->execute( $request_params );

/**
* Filters the resource contents after execution.
*
* Use this filter for content transformation, caching storage,
* PII redaction, or audit logging.
*
* @since n.e.x.t
*
* @param mixed $contents The raw resource contents.
* @param array $params The request parameters used.
* @param string $uri The resource URI.
* @param \WP\MCP\Domain\Resources\McpResource $mcp_resource The MCP resource instance.
* @param \WP\MCP\Core\McpServer $server The MCP server instance.
*/
$contents = apply_filters( 'mcp_adapter_resource_read_result', $contents, $request_params, $uri, $mcp_resource, $this->mcp );

// Handle WP_Error objects returned by McpResource execution.
if ( is_wp_error( $contents ) ) {
$this->mcp->error_handler->log(
Expand Down
55 changes: 55 additions & 0 deletions includes/Handlers/Tools/ToolsHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ public function list_all_tools(): ListToolsResult {
public function list_tools(): ListToolsResult {
$tools = array_values( $this->mcp->get_tools() );

/**
* Filters the list of tools before returning to the client.
*
* Use this filter to hide tools per user/role, add dynamic tools,
* or reorder the tools list.
*
* @since n.e.x.t
*
* @param array<\WP\McpSchema\Server\Tools\DTO\Tool> $tools Array of Tool DTOs.
* @param \WP\MCP\Core\McpServer $server The MCP server instance.
*/
$tools = apply_filters( 'mcp_adapter_tools_list', $tools, $this->mcp );

return ListToolsResult::fromArray(
array(
'tools' => $tools,
Expand Down Expand Up @@ -150,8 +163,50 @@ public function call_tool( array $params, $request_id = 0 ) {
);
}

/**
* Filters tool arguments before execution, or short-circuits execution entirely.
*
* Return the (optionally modified) arguments array to proceed with execution,
* or return a WP_Error to block execution and return an error to the client.
*
* @since n.e.x.t
*
* @param array $args The tool arguments.
* @param string $tool_name The tool name being called.
* @param \WP\MCP\Domain\Tools\McpTool $mcp_tool The MCP tool instance.
* @param \WP\MCP\Core\McpServer $server The MCP server instance.
*/
$args = apply_filters( 'mcp_adapter_pre_tool_call', $args, $tool_name, $mcp_tool, $this->mcp );

// Allow pre-filter to short-circuit execution by returning WP_Error.
if ( is_wp_error( $args ) ) {
return CallToolResult::fromArray(
array(
'content' => array( ContentBlockHelper::text( $args->get_error_message() ) ),
'structuredContent' => null,
'isError' => true,
)
);
}

$result = $mcp_tool->execute( $args );

/**
* Filters the tool execution result before response assembly.
*
* Use this filter for result transformation, PII redaction,
* audit logging, or content enrichment.
*
* @since n.e.x.t
*
* @param mixed $result The raw execution result.
* @param array $args The tool arguments used.
* @param string $tool_name The tool name that was called.
* @param \WP\MCP\Domain\Tools\McpTool $mcp_tool The MCP tool instance.
* @param \WP\MCP\Core\McpServer $server The MCP server instance.
*/
$result = apply_filters( 'mcp_adapter_tool_call_result', $result, $args, $tool_name, $mcp_tool, $this->mcp );

if ( is_wp_error( $result ) ) {
$this->mcp->error_handler->log(
'Tool execution returned WP_Error',
Expand Down
29 changes: 29 additions & 0 deletions tests/Unit/Handlers/InitializeHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,33 @@ public function test_handle_withEmptyVersion_negotiatesToLatest(): void {
// Verify other fields are still correct.
$this->assertSame( 'Test Server', $result->getServerInfo()->getName() );
}

public function test_handle_applies_initialize_response_filter(): void {
$server = new McpServer(
'test',
'mcp/v1',
'/mcp',
'Test Server',
'Original instructions',
'1.0.0',
array(),
DummyErrorHandler::class,
DummyObservabilityHandler::class,
);

$filter = static function ( InitializeResult $result ): InitializeResult {
$data = $result->toArray();
$data['instructions'] = 'Custom instructions';

return InitializeResult::fromArray( $data );
};
add_filter( 'mcp_adapter_initialize_response', $filter );

$handler = new InitializeHandler( $server );
$result = $handler->handle( McpConstants::LATEST_PROTOCOL_VERSION );

$this->assertSame( 'Custom instructions', $result->getInstructions() );

remove_filter( 'mcp_adapter_initialize_response', $filter );
}
}
Loading
Loading