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, annotations
  • McpTool::fromAbility( \WP_Ability $ability )
  • McpResource::fromArray( array $config ) — Same config keys: uri, name, title, description, handler, permission, mimeType, annotations
  • McpResource::fromAbility( \WP_Ability $ability, ?McpErrorHandlerInterface $error_handler = null )
  • McpPrompt::fromArray( array $config ) — Same config keys: name, title, description, arguments, handler, permission
  • McpPrompt::fromAbility( \WP_Ability $ability )
  • McpPrompt::fromBuilder( McpPromptBuilderInterface $builder )

Core API (unchanged):

  • McpAdapter::create_server() — Signature unchanged
  • McpAdapter::get_server(), McpAdapter::get_servers()

Public interfaces (unchanged):

  • McpErrorHandlerInterface::log( string $message, array $context = [], string $type = 'error' ): void
  • McpObservabilityHandlerInterface::record_event( string $event, array $tags = [], ?float $duration_ms = null ): void
  • McpTransportInterface and McpRestTransportInterface
  • McpPromptBuilderInterface

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 returns array — 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:

CategoryClass
ToolsWP\McpSchema\Server\Tools\DTO\Tool
ToolsWP\McpSchema\Server\Tools\DTO\ListToolsResult
ToolsWP\McpSchema\Server\Tools\DTO\CallToolResult
ToolsWP\McpSchema\Server\Tools\DTO\ToolAnnotations
ResourcesWP\McpSchema\Server\Resources\DTO\Resource
ResourcesWP\McpSchema\Server\Resources\DTO\ListResourcesResult
ResourcesWP\McpSchema\Server\Resources\DTO\ReadResourceResult
PromptsWP\McpSchema\Server\Prompts\DTO\Prompt
PromptsWP\McpSchema\Server\Prompts\DTO\ListPromptsResult
PromptsWP\McpSchema\Server\Prompts\DTO\GetPromptResult
PromptsWP\McpSchema\Server\Prompts\DTO\PromptMessage
PromptsWP\McpSchema\Server\Prompts\DTO\PromptArgument
ContentWP\McpSchema\Common\Content\DTO\TextContent
ContentWP\McpSchema\Common\Content\DTO\ImageContent
ContentWP\McpSchema\Common\Content\DTO\AudioContent
ProtocolWP\McpSchema\Common\Protocol\DTO\InitializeResult
ProtocolWP\McpSchema\Common\Protocol\DTO\Annotations
ProtocolWP\McpSchema\Common\Protocol\DTO\TextResourceContents
ProtocolWP\McpSchema\Common\Protocol\DTO\BlobResourceContents
ProtocolWP\McpSchema\Common\Protocol\DTO\EmbeddedResource
JSON-RPCWP\McpSchema\Common\JsonRpc\DTO\JSONRPCErrorResponse
JSON-RPCWP\McpSchema\Common\JsonRpc\DTO\Error
LifecycleWP\McpSchema\Common\Lifecycle\DTO\Implementation
LifecycleWP\McpSchema\Server\Lifecycle\DTO\ServerCapabilities
ConstantsWP\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 responses
  • execute( $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 clients
  • get_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 DTO
  • image() — ImageContent DTO
  • audio() — AudioContent DTO
  • embedded_text_resource() — EmbeddedResource with TextResourceContents
  • embedded_blob_resource() — EmbeddedResource with BlobResourceContents
  • json_text() — TextContent from JSON-encoded data
  • error_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 McpErrorFactory directly, 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:

HandlerMethodReturn type (v0.5.0)
ToolsHandlerlist_tools()WP\McpSchema\Server\Tools\DTO\ListToolsResult
ToolsHandlercall_tool()CallToolResult or JSONRPCErrorResponse
ResourcesHandlerlist_resources()WP\McpSchema\Server\Resources\DTO\ListResourcesResult
ResourcesHandlerread_resource()ReadResourceResult or JSONRPCErrorResponse
PromptsHandlerlist_prompts()WP\McpSchema\Server\Prompts\DTO\ListPromptsResult
PromptsHandlerget_prompt()GetPromptResult or JSONRPCErrorResponse
InitializeHandlerhandle()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