# Migration guide: version 0.5.0

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:

| 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 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:

```php
// 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):**
```php
// ToolsHandler returned arrays
$result = $tools_handler->list_tools();
// $result was: ['tools' => [['name' => 'my-tool', ...], ...]]
```

**After (v0.5.0):**
```php
// 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):**
```php
$error = McpErrorFactory::unauthorized( $request_id );
return new \WP_REST_Response( $error, 401 );
```

**After (v0.5.0):**
```php
$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`:

```php
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):**
```php
$tool_array = $tool->to_array();
$name = $tool_array['name'];
$meta = $tool_array['_meta'];
```

**After (v0.5.0):**
```php
$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.

```php
// 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()`:

```php
$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:**

```php
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:**

```php
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:**

```php
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:

```php
// 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`:

```php
// 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](../architecture/overview.md)** — Understand the Schema Layer / Adapter Layer separation
- **[Custom Transports](../guides/custom-transports.md)** — Custom transport implementation guide
- **[Error Handling](../guides/error-handling.md)** — Error handling patterns
