This document explains how the MCP Adapter transforms WordPress abilities into MCP components and handles requests from AI agents.
The MCP Adapter uses a two-layer architecture that separates protocol concerns from WordPress integration:
The Schema Layer is provided by the php-mcp-schema package (WP\McpSchema\ namespace). It contains protocol-only data transfer objects that are safe to expose to MCP clients.
Key DTOs:
| Category | Classes |
|---|---|
| Component definitions | Tool, Resource, Prompt, PromptArgument, ToolAnnotations, Annotations |
| Result types | ListToolsResult, CallToolResult, ListResourcesResult, ReadResourceResult, ListPromptsResult, GetPromptResult |
| Content blocks | TextContent, ImageContent, AudioContent, EmbeddedResource |
All DTOs extend AbstractDataTransferObject, which provides toArray() and fromArray() methods for serialization. These types carry no execution logic and no adapter-internal metadata.
The Adapter Layer wraps each protocol DTO with execution wiring and WordPress-specific metadata. Domain models McpTool, McpResource, and McpPrompt each implement the McpComponentInterface contract:
interface McpComponentInterface {
public function get_protocol_dto(): AbstractDataTransferObject;
public function execute( $arguments );
public function check_permission( $arguments );
public function get_adapter_meta(): array;
public function get_observability_context(): array;
}This separation ensures that:
- Protocol DTOs contain only fields defined by the MCP specification and are serialized directly into responses.
- Adapter metadata (ability references, schema transformation flags, permission callbacks) stays internal and is never exposed to MCP clients.
- Observability context provides structured tags for logging and metrics without polluting DTO
_meta.
McpComponentInterface is an internal contract (@internal). It is not intended for third-party implementation.
The remaining layers wire the Schema and Adapter layers together:
- Core:
McpAdapter(singleton registry),McpServer,McpComponentRegistry,McpTransportFactory - Handlers:
InitializeHandler,ToolsHandler,ResourcesHandler,PromptsHandler,SystemHandler - Transport:
HttpTransport, STDIO transport,RequestRouter - Infrastructure: Error handling (
McpErrorHandlerInterface), Observability (McpObservabilityHandlerInterface)
- Purpose: Central registry managing multiple MCP servers
- Key Methods:
create_server(),get_server(),get_servers(),instance() - Initialization: Hooks into
rest_api_initand firesmcp_adapter_initaction
- Purpose: Individual MCP server with specific configuration
- Components: Uses
McpComponentRegistryto manageMcpComponentInterfaceinstances - Typed access:
get_tools(),get_resources(),get_prompts()return component collections - Dependencies: Error handler, observability handler, transport permission callback
- Purpose: Stores and retrieves
McpComponentInterfaceinstances - Registration:
register_tools(),register_resources(),register_prompts()accept both ability names andMcpComponentInterfaceinstances - Name sanitization: Uses
McpNameSanitizerto normalize tool and prompt names - Validation: Validates components with
McpValidatorwhen validation is enabled
- Purpose: Creates transport instances with dependency injection
- Context Creation: Builds
McpTransportContextwith all required handlers - Validation: Ensures transport classes implement
McpTransportInterface
- Purpose: Routes MCP method calls to handlers that return schema DTOs
- DTO serialization boundary: Converts
AbstractDataTransferObjectresults to arrays viatoArray()andJSONRPCErrorResponseresults to error arrays - Observability: Extracts per-component context from
McpComponentInterface::get_observability_context()for request tagging
AI Agent --> Transport --> RequestRouter --> Handler --> McpComponentInterface --> Schema DTO --> Response
- Transport receives MCP request and authenticates
- RequestRouter maps method to appropriate handler
- Handler finds the
McpComponentInterfacecomponent, validates input, and invokes execution - Component delegates to a WordPress ability or direct callable, returning a result
- Handler wraps the result in a schema DTO (e.g.,
CallToolResult) - RequestRouter calls
toArray()on the DTO at the serialization boundary - Transport wraps the array in a JSON-RPC envelope and returns it
The RequestRouter maps MCP methods to handlers. All handlers return schema DTOs:
| Method | Handler | Return Type |
|---|---|---|
initialize |
InitializeHandler::handle() |
InitializeResult |
tools/list |
ToolsHandler::list_tools() |
ListToolsResult |
tools/call |
ToolsHandler::call_tool() |
CallToolResult or JSONRPCErrorResponse |
resources/list |
ResourcesHandler::list_resources() |
ListResourcesResult |
resources/read |
ResourcesHandler::read_resource() |
ReadResourceResult or JSONRPCErrorResponse |
prompts/list |
PromptsHandler::list_prompts() |
ListPromptsResult |
prompts/get |
PromptsHandler::get_prompt() |
GetPromptResult or JSONRPCErrorResponse |
ping |
SystemHandler::ping() |
Result |
Protocol-level errors (tool not found, missing parameters) return JSONRPCErrorResponse. Execution-level errors (permission denied, runtime failure) return the appropriate result DTO with isError: true.
WordPress abilities are converted to MCP components using factory methods on each domain model:
// Tool from ability
$tool = McpTool::fromAbility( $ability ); // Returns McpTool|WP_Error
// Resource from ability
$resource = McpResource::fromAbility( $ability ); // Returns McpResource|WP_Error
// Prompt from ability
$prompt = McpPrompt::fromAbility( $ability ); // Returns McpPrompt|WP_ErrorComponents can also be created directly without a WordPress ability:
$tool = McpTool::fromArray( [
'name' => 'my-tool',
'title' => 'My Tool',
'description' => 'Does something useful',
'inputSchema' => [ 'type' => 'object', 'properties' => [ ... ] ],
'handler' => fn( $args ) => [ 'result' => 'done' ],
'permission' => fn() => current_user_can( 'edit_posts' ),
'annotations' => [ 'readOnlyHint' => true ],
] );Each component exposes its clean protocol DTO for serialization:
$dto = $tool->get_protocol_dto(); // Returns WP\McpSchema\Server\Tools\DTO\Tool
$array = $dto->toArray(); // Protocol-safe array for JSON responsesThe DTO contains only MCP specification fields. Adapter metadata (ability reference, schema transformation flags) lives on the McpTool instance and is never serialized.
Normalizes component names to MCP-valid format per MCP 2025-11-25 spec.
- Charset:
A-Za-z0-9_.-only - Max length: 128 characters
- Transformations:
/to-, accent transliteration, invalid character replacement - Truncation: Long names are truncated with an MD5 hash suffix for uniqueness
- Usage: Applied automatically during tool and prompt registration (not used for resources, which use URIs)
$name = McpNameSanitizer::sanitize_name( 'my-plugin/action-name' );
// Returns: 'my-plugin-action-name'Factory for creating typed content block DTOs used in tool call results, prompt messages, and resource contents.
| Method | Returns | Purpose |
|---|---|---|
text( $text ) |
TextContent |
Plain text content |
json_text( $data, $flags ) |
TextContent |
JSON-encoded data as text (flags: JSON_* constants) |
image( $data, $mime_type ) |
ImageContent |
Base64-encoded image |
audio( $data, $mime_type ) |
AudioContent |
Base64-encoded audio |
embedded_text_resource( $uri, $text ) |
EmbeddedResource |
Text resource embedded in content |
embedded_blob_resource( $uri, $blob ) |
EmbeddedResource |
Binary resource embedded in content |
error_text( $message ) |
TextContent |
Semantic alias for error messages |
to_array_list( $blocks ) |
array[] |
Converts content block DTOs to arrays |
Normalizes arguments between MCP clients and WordPress abilities. MCP clients send {} (empty object) for tools without arguments, which PHP decodes as [] (empty array). Abilities without an input schema expect null, not an empty array. This normalizer bridges that gap.
$args = AbilityArgumentNormalizer::normalize( $ability, $args );Provides a centralized, stable vocabulary of failure reason constants for observability events. Categories include:
- Registration failures:
ABILITY_NOT_FOUND,DUPLICATE_URI,ABILITY_CONVERSION_FAILED - Permission failures:
PERMISSION_DENIED,PERMISSION_CHECK_FAILED,NO_PERMISSION_STRATEGY - Execution failures:
NOT_FOUND,EXECUTION_FAILED,EXECUTION_EXCEPTION - Validation failures:
MISSING_PARAMETER,INVALID_PARAMETER
Extended validation for MCP component data per the MCP 2025-11-25 specification:
validate_name()-- Name charset and length validationvalidate_resource_uri()-- URI format per RFC 3986validate_mime_type()-- MIME type format validationvalidate_icons_array()-- Icon object validation (src, mimeType, sizes, theme)get_annotation_validation_errors()-- Annotation field validation (audience, priority, lastModified)validate_base64()-- Base64 content validation
interface McpTransportInterface {
public function __construct( McpTransportContext $context );
public function register_routes(): void;
}
interface McpRestTransportInterface extends McpTransportInterface {
public function check_permission( WP_REST_Request $request );
public function handle_request( WP_REST_Request $request ): WP_REST_Response;
}- HttpTransport: Recommended (MCP Streamable HTTP compliant)
- STDIO Transport: Via WP-CLI commands
Transports and the RequestRouter receive all dependencies through McpTransportContext, which bundles the server instance, all handlers, the router, error handler, and observability handler.
The RequestRouter is the serialization boundary between typed DTOs and transport-level arrays:
- It dispatches to the appropriate handler, which returns an
AbstractDataTransferObjectorJSONRPCErrorResponse. - For success DTOs, it calls
toArray()and returns the resulting array. - For error DTOs, it extracts the error object and returns
['error' => ...]. - The transport wraps the array in the JSON-RPC 2.0 envelope.
- Error Response Creation:
McpErrorFactorycreatesJSONRPCErrorResponseDTOs for protocol errors - Error Logging:
McpErrorHandlerInterfaceimplementations log errors for monitoring
// Protocol error DTO (returned to clients via JSON-RPC)
$error_response = McpErrorFactory::tool_not_found( $request_id, $tool_name );
// Error logging (for monitoring)
$error_handler->log( 'Tool not found', [
'tool_name' => $tool_name,
'user_id' => get_current_user_id(),
'server_id' => $server_id,
], 'error' );- ErrorLogMcpErrorHandler: Logs to PHP error log
- NullMcpErrorHandler: No-op handler (default)
The system emits events rather than storing counters:
interface McpObservabilityHandlerInterface {
public function record_event( string $event, array $tags = [], ?float $duration_ms = null ): void;
}- Request events:
mcp.requestwith status, method, transport, and duration tags - Component events:
mcp.component.registered,mcp.component.registration_failed - Per-component context: Extracted from
McpComponentInterface::get_observability_context()and merged into request tags
class MyTransport implements McpRestTransportInterface {
use McpTransportHelperTrait;
private McpTransportContext $context;
public function __construct( McpTransportContext $context ) {
$this->context = $context;
}
public function register_routes(): void {
// Register custom REST routes
}
public function check_permission( WP_REST_Request $request ) {
return current_user_can( 'manage_options' );
}
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,
'my-transport'
);
return new WP_REST_Response( $result );
}
}class MyErrorHandler implements McpErrorHandlerInterface {
public function log( string $message, array $context = [], string $type = 'error' ): void {
MyMonitoringSystem::send( $message, $context, $type );
}
}class MyObservabilityHandler implements McpObservabilityHandlerInterface {
use McpObservabilityHelperTrait;
public function record_event( string $event, array $tags = [], ?float $duration_ms = null ): void {
$formatted_event = self::format_metric_name( $event );
$merged_tags = self::merge_tags( $tags );
MyMetricsSystem::counter( $formatted_event, 1, $merged_tags );
if ( null !== $duration_ms ) {
MyMetricsSystem::timing( $formatted_event, $duration_ms, $merged_tags );
}
}
}- Two-layer DTO separation: Protocol DTOs from
php-mcp-schemacarry no adapter-internal fields;get_protocol_dto()->toArray()always produces spec-compliant output - Dependency injection: All transports receive dependencies through
McpTransportContext; no global state beyond theMcpAdaptersingleton - Interface-based design: Error handlers, observability, and transports are all swappable via interfaces
- Event emission over counters: Observability emits events; external systems handle aggregation — zero overhead when disabled
- Lazy loading: Components created only when needed; validation disabled by default via
mcp_adapter_validation_enabledfilter
- Creating Abilities -- Build MCP components from WordPress abilities
- Custom Transports -- Implement specialized transport protocols
- Error Handling -- Custom error management
- Observability -- Metrics and monitoring
- v0.5.0 Migration Guide -- Upgrading from previous versions