Add mcp proxy command (#65)

This commit is contained in:
Pascal Birchler 2025-07-28 11:22:48 +02:00 committed by GitHub
parent 87fc7b6189
commit 1f540698bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 215 additions and 1 deletions

View file

@ -14,4 +14,5 @@ if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {

WP_CLI::add_command( 'ai', AiCommand::class );
WP_CLI::add_command( 'mcp prompt', AiCommand::class );
WP_CLI::add_command( 'mcp', McpCommand::class );
WP_CLI::add_command( 'mcp server', McpServerCommand::class );

View file

@ -7,7 +7,6 @@ use McpWp\AiCommand\AI\AiClient;
use McpWp\AiCommand\MCP\Client;
use McpWp\AiCommand\Utils\CliLogger;
use McpWp\AiCommand\Utils\McpConfig;
use McpWp\MCP\Server;
use McpWp\MCP\Servers\WordPress\WordPress;
use WP_CLI;
use WP_CLI\Utils;

115
src/MCP/ProxySession.php Normal file
View file

@ -0,0 +1,115 @@
<?php

namespace McpWp\AiCommand\MCP;

use InvalidArgumentException;
use Mcp\Client\ClientSession;
use Mcp\Server\InitializationOptions;
use Mcp\Server\ServerSession;
use Mcp\Server\Transport\Transport;
use Mcp\Shared\RequestResponder;
use Mcp\Types\CallToolRequest;
use Mcp\Types\CallToolResult;
use Mcp\Types\ClientRequest;
use Mcp\Types\CompleteRequest;
use Mcp\Types\CompleteResult;
use Mcp\Types\EmptyResult;
use Mcp\Types\GetPromptRequest;
use Mcp\Types\GetPromptResult;
use Mcp\Types\InitializeRequest;
use Mcp\Types\InitializeResult;
use Mcp\Types\ListPromptsRequest;
use Mcp\Types\ListPromptsResult;
use Mcp\Types\ListResourcesRequest;
use Mcp\Types\ListResourcesResult;
use Mcp\Types\ListToolsRequest;
use Mcp\Types\ListToolsResult;
use Mcp\Types\PingRequest;
use Mcp\Types\ReadResourceRequest;
use Mcp\Types\ReadResourceResult;
use Mcp\Types\SubscribeRequest;
use Mcp\Types\UnsubscribeRequest;
use Psr\Log\LoggerInterface;


/**
* ProxySession to pass messages from STDIO to an HTTP MCP server.
*/
class ProxySession extends ServerSession {
protected ?ClientSession $client_session;

public function __construct(
protected readonly string $url,
Transport $transport,
InitializationOptions $init_options,
?LoggerInterface $logger = null
) {
parent::__construct(
$transport,
$init_options,
$logger
);
}

/**
* Handle incoming requests. If it's the initialize request, handle it specially.
* Otherwise, ensure initialization is complete before handling other requests.
*
* @param ClientRequest $request The incoming client request.
* @param callable $respond The responder callable.
*/
public function handleRequest( RequestResponder $responder ): void {
$request = $responder->getRequest();
$actual_request = $request->getRequest();
$method = $actual_request->method;

$this->logger->info( 'Proxying request: ' . json_encode( $actual_request ) );

$result = null;

if ( ! isset( $this->client_session ) ) {
$client = new Client( $this->logger );
$this->client_session = $client->connect(
$this->url
);
}

switch ( get_class( $actual_request ) ) {
case InitializeRequest::class:
$result = $this->client_session->sendRequest( $actual_request, InitializeResult::class );
break;
case SubscribeRequest::class:
case UnsubscribeRequest::class:
case PingRequest::class:
$result = $this->client_session->sendRequest( $actual_request, EmptyResult::class );
break;
case ListResourcesRequest::class:
$result = $this->client_session->sendRequest( $actual_request, ListResourcesResult::class );
break;
case ListToolsRequest::class:
$result = $this->client_session->sendRequest( $actual_request, ListToolsResult::class );
break;
case CallToolRequest::class:
$result = $this->client_session->sendRequest( $actual_request, CallToolResult::class );
break;
case ReadResourceRequest::class:
$result = $this->client_session->sendRequest( $actual_request, ReadResourceResult::class );
break;
case ListPromptsRequest::class:
$result = $this->client_session->sendRequest( $actual_request, ListPromptsResult::class );
break;
case GetPromptRequest::class:
$result = $this->client_session->sendRequest( $actual_request, GetPromptResult::class );
break;
case CompleteRequest::class:
$result = $this->client_session->sendRequest( $actual_request, CompleteResult::class );
break;
}

if ( null === $result ) {
throw new InvalidArgumentException( "Unhandled proxied request for method: $method / " . get_class( $actual_request ) );
}

$responder->sendResponse( $result );
}
}

99
src/McpCommand.php Normal file
View file

@ -0,0 +1,99 @@
<?php

namespace McpWp\AiCommand;

use Exception;
use Mcp\Server\Server;
use Mcp\Server\Transport\StdioServerTransport;
use McpWp\AiCommand\MCP\ProxySession;
use McpWp\AiCommand\Utils\CliLogger;
use McpWp\AiCommand\Utils\McpConfig;
use WP_CLI;
use WP_CLI_Command;

/**
* MCP command.
*
* Manage MCP servers for use with WP-CLI and proxy requests to servers using the HTTP transport.
*/
class McpCommand extends WP_CLI_Command {
/**
* Proxy MCP requests to a given server.
*
* ## OPTIONS
*
* <server>
* : Name of an existing server to proxy requests to.
*
* ## EXAMPLES
*
* # Add server from URL.
* $ wp mcp server add "mywpserver" "https://example.com/wp-json/mcp/v1/mcp"
* Success: Server added.
*
* # Proxy requests to server
* $ wp mcp proxy "mywpserver"
*
* @when before_wp_load
*
* @param string[] $args Indexed array of positional arguments.
*/
public function proxy( $args ): void {
$server = $this->get_config()->get_server( $args[0] );

if ( null === $server ) {
WP_CLI::error( 'Server does not exist.' );
return;
}

$url = $server['server'];

if ( ! str_starts_with( $url, 'http://' ) && ! str_starts_with( $url, 'https://' ) ) {
WP_CLI::error( 'Server is not using HTTP transport.' );
return;
}

$logger = new CliLogger();

$server = new Server( $args[0], $logger );

try {
$transport = StdioServerTransport::create();

$proxy_session = new ProxySession(
$url,
$transport,
$server->createInitializationOptions(),
$logger
);

$server->setSession( $proxy_session );

$proxy_session->registerHandlers( $server->getHandlers() );
$proxy_session->registerNotificationHandlers( $server->getNotificationHandlers() );

$proxy_session->start();

$logger->info( 'Server started' );

} catch ( Exception $e ) {
$logger->error( 'Proxy error: ' . $e->getMessage() );
} finally {
if ( isset( $proxy_session ) ) {
$proxy_session->stop();
}
if ( isset( $transport ) ) {
$transport->stop();
}
}
}

/**
* Returns an McpConfig instance.
*
* @return McpConfig Config instance.
*/
protected function get_config(): McpConfig {
return new McpConfig();
}
}