FileMaster
Search
Toggle Dark Mode
Home
/
.
/
wp-content
/
plugins
/
woocommerce
/
vendor
/
wordpress
/
mcp-adapter
/
includes
/
Cli
Edit File: StdioServerBridge.php
<?php /** * STDIO Server Bridge for MCP Adapter * * This service acts as a bridge between the STDIO protocol and MCP servers, * allowing any registered server to be exposed via command-line interface. * * @package McpAdapter */ declare( strict_types=1 ); namespace WP\MCP\Cli; use WP\MCP\Core\McpServer; use WP\MCP\Transport\Infrastructure\RequestRouter; /** * STDIO Server Bridge - Exposes MCP servers via STDIO protocol * * This service handles JSON-RPC communication over stdin/stdout and delegates * requests to the appropriate MCP server. Unlike transport implementations, * this is a presentation layer service that can work with any server. */ class StdioServerBridge { /** * The MCP server to expose via STDIO. * * @var \WP\MCP\Core\McpServer */ private McpServer $server; /** * Request router for handling MCP requests. * * @var \WP\MCP\Transport\Infrastructure\RequestRouter */ private RequestRouter $request_router; /** * Whether the bridge is currently running. * * @var bool */ private bool $is_running = false; /** * Initialize the STDIO server bridge. * * @param \WP\MCP\Core\McpServer $server The MCP server to expose. */ public function __construct( McpServer $server ) { $this->server = $server; // Create request router using server's infrastructure $this->request_router = $this->create_request_router(); } /** * Start the STDIO server bridge. * * This method reads JSON-RPC messages from stdin and writes responses to stdout. * It runs in a loop until terminated or until it receives a shutdown signal. * * @throws \RuntimeException If STDIO transport is disabled. */ public function serve(): void { // Check if STDIO transport is enabled $enable_serve = apply_filters( 'mcp_adapter_enable_stdio_transport', true ); if ( ! $enable_serve ) { throw new \RuntimeException( 'The STDIO transport is disabled. Enable it by setting the "mcp_adapter_enable_stdio_transport" filter to true.' ); } $this->is_running = true; // Log to stderr to keep stdout clean for MCP messages $this->log_to_stderr( sprintf( 'MCP STDIO Bridge started for server: %s', $this->server->get_server_id() ) ); // Main server loop while ( $this->is_running ) { try { // Read a line from stdin (blocking) $input = fgets( STDIN ); if ( false === $input ) { // EOF or error reading from stdin break; } // Trim newline delimiter $input = rtrim( $input, "\r\n" ); if ( empty( $input ) ) { // Empty line, continue reading continue; } // Process the request and get response $response = $this->handle_request( $input ); // Write response to stdout with newline delimiter if ( ! empty( $response ) ) { // Use fwrite() for precise binary-safe JSON-RPC protocol communication. // WP_CLI output functions would add formatting/prefixes that break MCP protocol. // MCP requires exact control over stdout for machine-to-machine communication. fwrite( STDOUT, $response . "\n" ); // phpcs:ignore fflush( STDOUT ); } } catch ( \Throwable $e ) { // Log errors to stderr $this->log_to_stderr( 'Error processing request: ' . $e->getMessage() ); // Send error response $error_response = wp_json_encode( array( 'jsonrpc' => '2.0', 'error' => array( 'code' => -32603, 'message' => 'Internal error', 'data' => array( 'details' => $e->getMessage(), ), ), 'id' => null, ) ); fwrite( STDOUT, $error_response . "\n" ); // phpcs:ignore fflush( STDOUT ); } } $this->log_to_stderr( 'MCP STDIO Bridge stopped' ); } /** * Stop the STDIO server bridge. */ public function stop(): void { $this->is_running = false; } /** * Handle a JSON-RPC request string and return a JSON-RPC response string. * * @param string $json_input The JSON-RPC request string. * * @return string The JSON-RPC response string (empty for notifications). */ private function handle_request( string $json_input ): string { try { // Parse JSON-RPC request $request = json_decode( $json_input, true ); if ( json_last_error() !== JSON_ERROR_NONE ) { return $this->create_error_response( null, -32700, 'Parse error', 'Invalid JSON was received by the server.' ); } // Validate JSON-RPC structure if ( ! is_array( $request ) ) { return $this->create_error_response( null, -32600, 'Invalid Request', 'The JSON sent is not a valid Request object.' ); } // Check for JSON-RPC version if ( ! isset( $request['jsonrpc'] ) || '2.0' !== $request['jsonrpc'] ) { return $this->create_error_response( $request['id'] ?? null, -32600, 'Invalid Request', 'The JSON-RPC version must be 2.0.' ); } // Extract request components $method = $request['method'] ?? null; $params = $request['params'] ?? array(); $id = $request['id'] ?? null; if ( ! is_string( $method ) ) { return $this->create_error_response( $id, -32600, 'Invalid Request', 'Method must be a string.' ); } // Convert params to array if it's an object if ( is_object( $params ) ) { $params = (array) $params; } if ( ! is_array( $params ) ) { $params = array(); } // Route the request to the appropriate handler $result = $this->request_router->route_request( $method, $params, $id, 'stdio' ); // If this is a notification (no id), don't send a response if ( null === $id ) { return ''; } // Format the response return $this->format_response( $result, $id ); } catch ( \Throwable $e ) { // Handle unexpected errors return $this->create_error_response( null, -32603, 'Internal error', $e->getMessage() ); } } /** * Format a handler result as a JSON-RPC response. * * @param array $result The handler result. * @param mixed $id The request ID. * * @return string The JSON-RPC response string. */ private function format_response( array $result, $id ): string { $response = array( 'jsonrpc' => '2.0', 'id' => $id, ); // Check if result contains an error if ( isset( $result['error'] ) ) { $error = $result['error']; // Ensure error has required fields $response['error'] = array( 'code' => $error['code'] ?? -32603, 'message' => $error['message'] ?? 'Internal error', ); // Add data field if present if ( isset( $error['data'] ) ) { $response['error']['data'] = $error['data']; } } else { // Success response $response['result'] = (object) $result; } $json = wp_json_encode( $response, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); if ( false === $json ) { // Fallback for encoding errors return $this->create_error_response( $id, -32603, 'Internal error', 'Failed to encode response as JSON.' ); } return $json; } /** * Create a JSON-RPC error response. * * @param mixed $id The request ID (can be null). * @param int $code The error code. * @param string $message The error message. * @param string $data Optional error data. * * @return string The JSON error response string. */ private function create_error_response( $id, int $code, string $message, string $data = '' ): string { $response = array( 'jsonrpc' => '2.0', 'error' => array( 'code' => $code, 'message' => $message, ), 'id' => $id, ); if ( ! empty( $data ) ) { $response['error']['data'] = $data; } return wp_json_encode( $response, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) ?: '{"jsonrpc":"2.0","error":{"code":-32603,"message":"Internal error"},"id":null}'; } /** * Create a request router for the server. * * @return \WP\MCP\Transport\Infrastructure\RequestRouter */ private function create_request_router(): RequestRouter { // Create transport context using server's infrastructure $context = $this->server->create_transport_context(); return $context->request_router; } /** * Log a message to stderr. * * @param string $message The message to log. */ private function log_to_stderr( string $message ): void { fwrite( STDERR, "[MCP STDIO Bridge] $message\n" ); // phpcs:ignore } /** * Get the server this bridge is exposing. * * @return \WP\MCP\Core\McpServer */ public function get_server(): McpServer { return $this->server; } }
Save
Back