Migration guide: version 0.5.0
March 26, 2026 · View on GitHub
Version 0.5.0 introduces typed protocol DTOs via php-mcp-schema, replacing internal array manipulation with schema-validated data structures. The architecture now cleanly separates the Schema Layer (protocol types from WP\McpSchema\) from the Adapter Layer (WordPress integration in WP\MCP\).
For most users: seamless upgrade
If you register abilities, create servers via create_server(), or use WordPress hooks — nothing breaks. Update the plugin and everything continues to work.
The following public APIs are unchanged in v0.5.0:
Component registration (unchanged):
McpTool::fromArray( array $config )— Same config keys:name,title,description,inputSchema,handler,permission,annotationsMcpTool::fromAbility( \WP_Ability $ability )McpResource::fromArray( array $config )— Same config keys:uri,name,title,description,handler,permission,mimeType,annotationsMcpResource::fromAbility( \WP_Ability $ability, ?McpErrorHandlerInterface $error_handler = null )McpPrompt::fromArray( array $config )— Same config keys:name,title,description,arguments,handler,permissionMcpPrompt::fromAbility( \WP_Ability $ability )McpPrompt::fromBuilder( McpPromptBuilderInterface $builder )
Core API (unchanged):
McpAdapter::create_server()— Signature unchangedMcpAdapter::get_server(),McpAdapter::get_servers()
Public interfaces (unchanged):
McpErrorHandlerInterface::log( string $message, array $context = [], string $type = 'error' ): voidMcpObservabilityHandlerInterface::record_event( string $event, array $tags = [], ?float $duration_ms = null ): voidMcpTransportInterfaceandMcpRestTransportInterfaceMcpPromptBuilderInterface
WordPress hooks (unchanged):
- All action and filter hook names unchanged
- Hook parameters unchanged
mcp_adapter_init,wp_mcp_init, and all filters listed in the documentation
Transport return format (unchanged):
RequestRouter::route_request()still returnsarray— DTOs are converted to arrays internally- Error shape:
['error' => ['code' => int, 'message' => string]] - Success shape: the result DTO's
toArray()output
New features
php-mcp-schema Package
New dependency: wordpress/php-mcp-schema ^0.1.0. This package provides typed DTOs for all MCP protocol types under the WP\McpSchema\ namespace.
Key DTO classes used throughout the adapter:
| Category | Class |
|---|---|
| Tools | WP\McpSchema\Server\Tools\DTO\Tool |
| Tools | WP\McpSchema\Server\Tools\DTO\ListToolsResult |
| Tools | WP\McpSchema\Server\Tools\DTO\CallToolResult |
| Tools | WP\McpSchema\Server\Tools\DTO\ToolAnnotations |
| Resources | WP\McpSchema\Server\Resources\DTO\Resource |
| Resources | WP\McpSchema\Server\Resources\DTO\ListResourcesResult |
| Resources | WP\McpSchema\Server\Resources\DTO\ReadResourceResult |
| Prompts | WP\McpSchema\Server\Prompts\DTO\Prompt |
| Prompts | WP\McpSchema\Server\Prompts\DTO\ListPromptsResult |
| Prompts | WP\McpSchema\Server\Prompts\DTO\GetPromptResult |
| Prompts | WP\McpSchema\Server\Prompts\DTO\PromptMessage |
| Prompts | WP\McpSchema\Server\Prompts\DTO\PromptArgument |
| Content | WP\McpSchema\Common\Content\DTO\TextContent |
| Content | WP\McpSchema\Common\Content\DTO\ImageContent |
| Content | WP\McpSchema\Common\Content\DTO\AudioContent |
| Protocol | WP\McpSchema\Common\Protocol\DTO\InitializeResult |
| Protocol | WP\McpSchema\Common\Protocol\DTO\Annotations |
| Protocol | WP\McpSchema\Common\Protocol\DTO\TextResourceContents |
| Protocol | WP\McpSchema\Common\Protocol\DTO\BlobResourceContents |
| Protocol | WP\McpSchema\Common\Protocol\DTO\EmbeddedResource |
| JSON-RPC | WP\McpSchema\Common\JsonRpc\DTO\JSONRPCErrorResponse |
| JSON-RPC | WP\McpSchema\Common\JsonRpc\DTO\Error |
| Lifecycle | WP\McpSchema\Common\Lifecycle\DTO\Implementation |
| Lifecycle | WP\McpSchema\Server\Lifecycle\DTO\ServerCapabilities |
| Constants | WP\McpSchema\Common\McpConstants |
All DTOs provide fromArray() for construction and toArray() for serialization.
McpComponentInterface
New internal contract (WP\MCP\Domain\Contracts\McpComponentInterface) that all domain components implement. Provides:
get_protocol_dto()— Clean schema DTO for MCP client responsesexecute( $arguments )— Unified execution surface (delegates to ability or callable handler)check_permission( $arguments )— Unified permission check (delegates to ability or callback)get_adapter_meta()— Internal metadata not exposed to clientsget_observability_context()— Tags for logging and metrics
New utility classes
McpNameSanitizer (WP\MCP\Domain\Utils\McpNameSanitizer)
Normalizes component names to MCP-valid format (A-Za-z0-9_.- , max 128 characters). Handles transliteration, invalid character replacement, and truncation with hash suffixes.
ContentBlockHelper (WP\MCP\Domain\Utils\ContentBlockHelper)
Factory for creating MCP content block DTOs. Provides convenience methods:
text()— TextContent DTOimage()— ImageContent DTOaudio()— AudioContent DTOembedded_text_resource()— EmbeddedResource with TextResourceContentsembedded_blob_resource()— EmbeddedResource with BlobResourceContentsjson_text()— TextContent from JSON-encoded dataerror_text()— TextContent for error messages
AbilityArgumentNormalizer (WP\MCP\Domain\Utils\AbilityArgumentNormalizer)
Normalizes arguments between MCP clients and WordPress Abilities API. Converts empty arrays (from MCP {}) to null for abilities without input schemas.
FailureReason (WP\MCP\Infrastructure\Observability\FailureReason)
Standardized failure reason constants for observability events. Replaces string literals with a typed taxonomy:
// Before: string literals scattered across the codebase
'failure_reason' => 'permission_denied'
// After: centralized constants
'failure_reason' => FailureReason::PERMISSION_DENIED
Categories: registration failures (ABILITY_NOT_FOUND, DUPLICATE_URI, BUILDER_EXCEPTION, NO_PERMISSION_STRATEGY, ABILITY_CONVERSION_FAILED), permission failures (PERMISSION_DENIED, PERMISSION_CHECK_FAILED), execution failures (NOT_FOUND, EXECUTION_FAILED, EXECUTION_EXCEPTION), and validation failures (MISSING_PARAMETER, INVALID_PARAMETER).
Advanced: internal API changes
This section only applies if you have custom handlers, custom transports that call
McpErrorFactorydirectly, or code that accesses internal component data structures. If you only use the public APIs listed above, you can skip this section entirely.
Custom handlers must return DTOs
All handlers now return typed schema DTOs instead of raw PHP arrays. If you created custom handlers that the RequestRouter calls, they must return DTOs.
Before (v0.4.x):
// ToolsHandler returned arrays
$result = $tools_handler->list_tools();
// $result was: ['tools' => [['name' => 'my-tool', ...], ...]]
After (v0.5.0):
// ToolsHandler returns DTOs
$result = $tools_handler->list_tools();
// $result is: ListToolsResult DTO
All affected handlers and their new return types:
| Handler | Method | Return type (v0.5.0) |
|---|---|---|
ToolsHandler | list_tools() | WP\McpSchema\Server\Tools\DTO\ListToolsResult |
ToolsHandler | call_tool() | CallToolResult or JSONRPCErrorResponse |
ResourcesHandler | list_resources() | WP\McpSchema\Server\Resources\DTO\ListResourcesResult |
ResourcesHandler | read_resource() | ReadResourceResult or JSONRPCErrorResponse |
PromptsHandler | list_prompts() | WP\McpSchema\Server\Prompts\DTO\ListPromptsResult |
PromptsHandler | get_prompt() | GetPromptResult or JSONRPCErrorResponse |
InitializeHandler | handle() | WP\McpSchema\Common\Protocol\DTO\InitializeResult |
RequestRouter checks the return type and will log a warning if a handler returns a non-DTO type.
McpErrorFactory returns DTOs
McpErrorFactory methods now return JSONRPCErrorResponse DTOs instead of arrays. This only affects you if you call McpErrorFactory directly (e.g., in a custom transport for pre-routing errors).
Before (v0.4.x):
$error = McpErrorFactory::unauthorized( $request_id );
return new \WP_REST_Response( $error, 401 );
After (v0.5.0):
$error = McpErrorFactory::unauthorized( $request_id );
return new \WP_REST_Response( $error->toArray(), 401 );
All McpErrorFactory static methods are affected:
parse_error(),invalid_request(),method_not_found(),invalid_params(),internal_error()tool_not_found(),resource_not_found(),prompt_not_found()permission_denied(),unauthorized(),mcp_disabled()validation_error(),missing_parameter(),ability_not_found()create_error_response()(the underlying factory method)
Error constant values are unchanged (PARSE_ERROR, INVALID_REQUEST, etc.) but are now also available from McpConstants:
use WP\McpSchema\Common\McpConstants;
McpErrorFactory::PARSE_ERROR; // -32700
McpConstants::PARSE_ERROR; // -32700
McpErrorFactory::get_http_status_for_error() accepts both DTOs and legacy arrays, so existing HTTP status logic continues to work.
Component internal data structure changed
Domain models (McpTool, McpResource, McpPrompt) now implement McpComponentInterface. This only affects you if your code accessed internal _meta arrays or component data directly.
Before (v0.4.x):
$tool_array = $tool->to_array();
$name = $tool_array['name'];
$meta = $tool_array['_meta'];
After (v0.5.0):
$tool_dto = $mcp_tool->get_protocol_dto();
$name = $tool_dto->getName();
$meta = $mcp_tool->get_adapter_meta();
McpComponentRegistry::get_tools() now returns protocol DTOs (via get_protocol_dto()). Use get_mcp_tool() to get the full McpTool instance with execution and permission methods.
Observability context changed
Observability context is now provided by McpComponentInterface::get_observability_context() instead of being extracted from response _metadata arrays.
// Before (v0.4.x)
$tags = $response['_metadata'] ?? [];
// After (v0.5.0)
$tags = $mcp_tool->get_observability_context();
// Returns: ['component_type' => 'tool', 'tool_name' => '...', 'source' => 'ability']
Failure reasons now use FailureReason constants instead of string literals. The string values are the same ('permission_denied', 'not_found', etc.) so filtering logic continues to work.
Migration steps for advanced users
Skip this section if you only use the public APIs (component registration,
create_server(), hooks).
Step 1: Custom transports
If you implemented McpTransportInterface or McpRestTransportInterface, the route_request() return format is unchanged (still arrays). Your transport code should work as-is.
Only update if you call McpErrorFactory directly for pre-routing errors — add ->toArray():
$error = McpErrorFactory::unauthorized( $request_id );
return new \WP_REST_Response( $error->toArray(), 401 );
Step 2: Custom handlers
If you created custom handlers called by RequestRouter, they must return schema DTOs:
Tools handler example:
use WP\McpSchema\Server\Tools\DTO\ListToolsResult;
use WP\McpSchema\Server\Tools\DTO\CallToolResult;
use WP\MCP\Domain\Utils\ContentBlockHelper;
use WP\MCP\Infrastructure\ErrorHandling\McpErrorFactory;
public function list_tools(): ListToolsResult {
$tools = array_values( $this->mcp->get_tools() );
return ListToolsResult::fromArray( [ 'tools' => $tools ] );
}
public function call_tool( array $params, $request_id = 0 ) {
if ( ! isset( $params['name'] ) ) {
return McpErrorFactory::missing_parameter( $request_id, 'tool name' );
}
return CallToolResult::fromArray( [
'content' => [ ContentBlockHelper::text( $json_text ) ],
'isError' => false,
] );
}
Resources handler example:
use WP\McpSchema\Server\Resources\DTO\ListResourcesResult;
use WP\McpSchema\Server\Resources\DTO\ReadResourceResult;
use WP\McpSchema\Common\Protocol\DTO\TextResourceContents;
public function list_resources(): ListResourcesResult {
$resources = array_values( $this->mcp->get_resources() );
return ListResourcesResult::fromArray( [ 'resources' => $resources ] );
}
public function read_resource( array $params, $request_id = 0 ) {
$content = TextResourceContents::fromArray( [
'uri' => $uri,
'text' => $text,
] );
return ReadResourceResult::fromArray( [ 'contents' => [ $content ] ] );
}
Prompts handler example:
use WP\McpSchema\Server\Prompts\DTO\ListPromptsResult;
use WP\McpSchema\Server\Prompts\DTO\GetPromptResult;
use WP\McpSchema\Server\Prompts\DTO\PromptMessage;
public function list_prompts(): ListPromptsResult {
$prompts = array_values( $this->mcp->get_prompts() );
return ListPromptsResult::fromArray( [ 'prompts' => $prompts ] );
}
public function get_prompt( array $params, $request_id = 0 ) {
$message = PromptMessage::fromArray( [
'role' => 'user',
'content' => [ 'type' => 'text', 'text' => $text ],
] );
return GetPromptResult::fromArray( [
'messages' => [ $message ],
'description' => $description,
] );
}
Step 3: Error handling code
If your code consumed McpErrorFactory return values as arrays:
// Before (v0.4.x)
$error = McpErrorFactory::internal_error( $request_id, 'Something went wrong' );
$error_code = $error['error']['code'];
// After (v0.5.0)
$error = McpErrorFactory::internal_error( $request_id, 'Something went wrong' );
$error_code = $error->getError()->getCode();
// Or convert to array first
$error_array = $error->toArray();
$error_code = $error_array['error']['code'];
Step 4: Observability code
If you have custom observability handlers that extracted tags from response _metadata:
// Before (v0.4.x)
$tags = $response['_metadata'] ?? [];
$component_type = $tags['component_type'] ?? 'unknown';
// After (v0.5.0)
$tags = $mcp_tool->get_observability_context();
Failure reason string values are unchanged — FailureReason::PERMISSION_DENIED resolves to 'permission_denied'. Use FailureReason::all() to get all valid values and FailureReason::is_valid() to validate.
Next steps
- Architecture Overview — Understand the Schema Layer / Adapter Layer separation
- Custom Transports — Custom transport implementation guide
- Error Handling — Error handling patterns