Migration Guide: Version 0.3.0

October 30, 2025 · View on GitHub

Version 0.3.0 introduces significant improvements to the observability system and streamlines the transport layer with a unified HTTP transport implementation.

What's Changed

Observability System Refactoring

The observability system has been completely refactored to use a metadata-driven architecture with consistent event naming and status tags.

Breaking Changes

1. Unified Event Names with Status Tags (MAJOR CHANGE)

All events now use consistent base names with a status tag instead of separate event names for success/failure.

Before (v0.2.x):

// Different event names for success and failure
$handler->record_event('mcp.request.success', ['method' => 'tools/call']);
$handler->record_event('mcp.request.error', ['method' => 'tools/call']);

$handler->record_event('mcp.tool.execution_success', ['tool_name' => 'my-tool']);
$handler->record_event('mcp.tool.execution_failed', ['tool_name' => 'my-tool']);

$handler->record_event('mcp.component.registered', ['component_type' => 'tool']);
$handler->record_event('mcp.component.registration_failed', ['component_type' => 'tool']);

After (v0.3.0):

// Single event name with status tag
$handler->record_event('mcp.request', ['status' => 'success', 'method' => 'tools/call']);
$handler->record_event('mcp.request', ['status' => 'error', 'method' => 'tools/call']);

// Tool events are consolidated into request events with metadata
$handler->record_event('mcp.request', [
    'status' => 'success',
    'method' => 'tools/call',
    'component_type' => 'tool',
    'tool_name' => 'my-tool',
    'ability_name' => 'my_ability'
]);

$handler->record_event('mcp.component.registration', ['status' => 'success', 'component_type' => 'tool']);
$handler->record_event('mcp.component.registration', ['status' => 'failed', 'component_type' => 'tool']);

Benefits:

  • Easier filtering: Query all requests with one event name, filter by status
  • Better grouping: Calculate success rates easily
  • Consistent API: Same pattern everywhere
  • Richer context: Tool/prompt/resource metadata automatically included

Migration for Monitoring Systems:

If you're using external monitoring systems that query event names:

-- Before: Query success events
SELECT * FROM events WHERE event_name = 'mcp.request.success'

-- After: Query with status filter
SELECT * FROM events WHERE event_name = 'mcp.request' AND tags->>'status' = 'success'

2. Metadata-Driven Observability

Observability events are now recorded centrally at the transport layer (RequestRouter) instead of in individual handlers. Handlers attach _metadata to responses, which flows up to the transport layer.

Impact: If you've created custom MCP handlers (Tools, Prompts, Resources), no migration needed - the system is backward compatible. However, if you were manually calling observability_handler->record_event() in custom code, update to return _metadata instead.

Before (v0.2.x):

class CustomToolsHandler {
    public function call_tool(array $params): array {
        // Manual observability call
        $this->observability_handler->record_event(
            'mcp.tool.execution_success',
            ['tool_name' => $params['name']]
        );
        
        return ['result' => 'success'];
    }
}

After (v0.3.0):

class CustomToolsHandler {
    public function call_tool(array $params): array {
        // Return metadata instead
        return [
            'result' => 'success',
            '_metadata' => [
                'component_type' => 'tool',
                'tool_name' => $params['name'],
            ]
        ];
    }
}

The RequestRouter automatically:

  • Extracts _metadata from responses
  • Merges with request context (method, transport, server_id)
  • Records event with duration timing
  • Strips _metadata before returning to client

3. Removed Helper Method

Removed: McpObservabilityHelperTrait::record_error_event()

This helper method appended _failed suffix to event names, which conflicts with the new status tag pattern.

Before (v0.2.x):

$this->record_error_event('mcp.tool.execution', $exception, ['tool_name' => 'my-tool']);
// Created event: mcp.tool.execution_failed

After (v0.3.0):

// Use standard record_event with status and error categorization
$this->record_event('mcp.request', [
    'status' => 'error',
    'tool_name' => 'my-tool',
    'error_type' => get_class($exception),
    'error_category' => self::categorize_error($exception),
]);

Note: categorize_error() method is still available in the helper trait.

4. Enhanced Event Tags

All events now include richer context automatically:

Request Events (mcp.request):

  • status: success | error
  • method: MCP method name
  • transport: Transport type
  • server_id: Server ID
  • component_type: Tool/resource/prompt (when applicable)
  • tool_name, ability_name, prompt_name, resource_uri: Component details
  • failure_reason: Specific failure reason (not_found, permission_denied, execution_failed, etc.)
  • error_code, error_type, error_category: Error details

Component Registration Events (mcp.component.registration):

  • status: success | failed
  • component_type: Tool/resource/prompt type
  • component_name: Component name
  • server_id: Server ID
  • error_type: Exception type (for failures)

Session and Request Tracking:

  • request_id: JSON-RPC request ID for request correlation
  • session_id: MCP session ID (null for non-session transports like CLI)
  • new_session_id: Newly created session ID (only on initialize)
  • params: Sanitized request parameters (tool names, URIs, argument counts)

Permission Error Details:

When WordPress abilities return WP_Error from has_permission(), the specific error message and code are now automatically extracted and used:

// Example: Ability returns WP_Error with validation details
$wp_error = new WP_Error(
    'ability_invalid_input',
    'Ability "wpcom-mcp/user-notifications" has invalid input. Reason: input[action] is not one of list, get_settings, get_devices, and test_delivery.'
);

// Old behavior: 
//   - Error message: Generic "Access denied for tool: X"
//   - failure_reason: Always "permission_denied"

// New behavior:
//   - Error message: Full WP_Error message with details
//   - failure_reason: WP_Error code ("ability_invalid_input")

Benefits:

  • More specific failure reasons in logs (e.g., ability_invalid_input vs generic permission_denied)
  • Easier to track and alert on specific permission failure types
  • Error messages include full validation context from abilities
  • Can monitor specific error patterns (rate limits, quota exceeded, etc.)

5. Instance-Based Handlers (No Longer Static)

Observability handlers now use instance methods instead of static methods, matching the error handler pattern.

Before (v0.2.x):

// Handlers were passed as class names
$adapter->create_server(
    'my-server',
    'mcp/v1',
    '/mcp',
    'My Server',
    'Description',
    '1.0.0',
    [ HttpTransport::class ],
    ErrorLogMcpErrorHandler::class,
    NullMcpObservabilityHandler::class  // Class name
);

// Static method calls
MyObservabilityHandler::record_event('event.name', ['tag' => 'value']);
MyObservabilityHandler::record_timing('metric.name', 123.45, ['tag' => 'value']);

After (v0.3.0):

// Handlers are still passed as class names to create_server
// (the server instantiates them internally)
$adapter->create_server(
    'my-server',
    'mcp/v1',
    '/mcp',
    'My Server',
    'Description',
    '1.0.0',
    [ HttpTransport::class ],
    ErrorLogMcpErrorHandler::class,
    NullMcpObservabilityHandler::class  // Still class name, instantiated by server
);

// Instance method calls (when implementing custom handlers)
class MyObservabilityHandler implements McpObservabilityHandlerInterface {
    public function record_event(string $event, array $tags = [], ?float $duration_ms = null): void {
        // Implementation
    }
}

2. Unified Event/Timing Interface

The record_timing() method has been removed. Use record_event() with the optional $duration_ms parameter instead.

Before (v0.2.x):

// Separate methods for events and timing
$handler::record_event('mcp.request.success', ['method' => 'tools/call']);
$handler::record_timing('mcp.request.duration', 45.23, ['method' => 'tools/call']);

// This created 2 separate log entries

After (v0.3.0):

// Unified method with optional duration parameter
$handler->record_event('mcp.request.success', ['method' => 'tools/call'], 45.23);

// This creates 1 log entry with timing included
// Output: [MCP Observability] EVENT mcp.request.success 45.23ms [method=tools/call,...]

3. Removed Events

The following events have been removed to reduce log volume:

  • mcp.request.count - No longer emitted (redundant with success/error events)

Before: Each request generated 3-4 log entries:

  • mcp.request.count
  • mcp.request.success OR mcp.request.error
  • mcp.request.duration

After: Each request generates 1 log entry:

  • mcp.request.success (with duration) OR mcp.request.error (with duration)

Migration Steps for Custom Handlers

If you have custom observability handlers, update them:

Step 1: Convert from static to instance methods

// Before
class MyHandler implements McpObservabilityHandlerInterface {
    public static function record_event(string $event, array $tags = []): void {
        // ...
    }
    
    public static function record_timing(string $metric, float $duration_ms, array $tags = []): void {
        // ...
    }
}

// After
class MyHandler implements McpObservabilityHandlerInterface {
    public function record_event(string $event, array $tags = [], ?float $duration_ms = null): void {
        // Handle both events and timing in one method
        // If $duration_ms is not null, include it in your tracking
    }
}

Step 2: Remove record_timing() method

The record_timing() method no longer exists in the interface. Consolidate all tracking into record_event().

Step 3: Update Helper Trait Usage

If using McpObservabilityHelperTrait::record_error_event(), it's now an instance method:

// Before
static::record_error_event('operation', $exception, ['context' => 'value']);

// After
$this->record_error_event('operation', $exception, ['context' => 'value']);

Transport Layer Changes

Removed Transports

The following transport classes have been removed:

Unified HTTP Transport

HttpTransport is now the sole HTTP transport implementation:

  • ✅ Full MCP 2025-06-18 specification compliance
  • ✅ Supports both WordPress REST API and JSON-RPC 2.0 formats
  • ✅ Handles streaming responses (SSE) and standard JSON responses
  • ✅ Built-in session management and batch request support

Using HttpTransport

All MCP servers use HttpTransport by default:

use WP\MCP\Core\McpAdapter;
use WP\MCP\Transport\HttpTransport;

add_action('mcp_adapter_init', function($adapter) {
    $adapter->create_server(
        'my-server-id',
        'my-namespace',
        'mcp',
        'My MCP Server',
        'Server description',
        '1.0.0',
        [ HttpTransport::class ],  // Use HttpTransport
        ErrorLogMcpErrorHandler::class,
        ['my-plugin/my-ability']
    );
});

Advanced Usage

Custom Authentication

Use transport permission callbacks for custom authentication:

add_action('mcp_adapter_init', function($adapter) {
    $adapter->create_server(
        'secure-server',
        'secure',
        'mcp',
        'Secure Server',
        'Custom auth example',
        '1.0.0',
        [ HttpTransport::class ],
        ErrorLogMcpErrorHandler::class,
        NullMcpObservabilityHandler::class,
        ['my-plugin/ability'],
        [],  // resources
        [],  // prompts
        function() {
            // Custom permission logic
            $api_key = $_SERVER['HTTP_X_API_KEY'] ?? '';
            return validate_api_key($api_key);
        }
    );
});

See the Transport Permissions Guide for more authentication patterns.

Custom Transport Implementations

For specialized requirements (message queues, custom protocols, etc.), create custom transports:

use WP\MCP\Transport\Contracts\McpRestTransportInterface;
use WP\MCP\Transport\Infrastructure\McpTransportContext;
use WP\MCP\Transport\Infrastructure\McpTransportHelperTrait;

class MyCustomTransport implements McpRestTransportInterface {
    use McpTransportHelperTrait;
    
    private McpTransportContext $context;
    
    public function __construct(McpTransportContext $context) {
        $this->context = $context;
        $this->register_routes();
    }
    
    public function register_routes(): void {
        // Register your custom routes
    }
    
    public function check_permission(\WP_REST_Request $request) {
        return is_user_logged_in();
    }
    
    public function handle_request(\WP_REST_Request $request): \WP_REST_Response {
        $body = $request->get_json_params();
        
        $result = $this->context->request_router->route_request(
            $body['method'],
            $body['params'] ?? [],
            $body['id'] ?? 0,
            $this->get_transport_name()
        );
        
        return rest_ensure_response($result);
    }
}

See the Custom Transports Guide for detailed implementation instructions.

Next Steps