Import code from mcp-server / de-duplication (#51)

* Import mcp-server repo with php-scoper

* exclude folder
This commit is contained in:
Pascal Birchler 2025-04-03 23:05:09 +02:00 committed by GitHub
parent feed34e25e
commit 37572bd180
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 140 additions and 1201 deletions

View file

@ -53,7 +53,7 @@ jobs:
custom-cache-suffix: $(date -u "+%Y-%m") custom-cache-suffix: $(date -u "+%Y-%m")


- name: Run Linter - name: Run Linter
run: vendor/bin/parallel-lint -j 10 . --show-deprecated --exclude vendor --exclude .git --checkstyle | cs2pr run: vendor/bin/parallel-lint -j 10 . --show-deprecated --exclude vendor --exclude .git --exclude third-party --checkstyle | cs2pr


phpcs: phpcs:
name: PHPCS name: PHPCS

5
.gitignore vendored
View file

@ -2,6 +2,9 @@
wp-cli.local.yml wp-cli.local.yml
node_modules/ node_modules/
vendor/ vendor/
src/vendor/
src/composer.json
third-party/
*.zip *.zip
*.tar.gz *.tar.gz
*.swp *.swp
@ -11,4 +14,4 @@ composer.lock
phpunit.xml phpunit.xml
phpcs.xml phpcs.xml
.phpcs.xml .phpcs.xml
.vscode/ .vscode/

View file

@ -33,7 +33,7 @@ Tip: for better on support of the latest PHP versions, use the v2.12 nightly bui
To install the latest development version of this package, use the following command instead: To install the latest development version of this package, use the following command instead:


```bash ```bash
wp package install swissspidy/ai-command:dev-main wp package install mcp-wp/ai-command:dev-main
``` ```


Right now, the plugin requires a WordPress site with the [AI Services plugin](https://wordpress.org/plugins/ai-services) installed. Right now, the plugin requires a WordPress site with the [AI Services plugin](https://wordpress.org/plugins/ai-services) installed.
@ -165,13 +165,13 @@ For a more thorough introduction, [check out WP-CLI's guide to contributing](htt


Think youve found a bug? Wed love for you to help us get it fixed. Think youve found a bug? Wed love for you to help us get it fixed.


Before you create a new issue, you should [search existing issues](https://github.com/swissspidy/ai-command/issues?q=label%3Abug%20) to see if theres an existing resolution to it, or if its already been fixed in a newer version. Before you create a new issue, you should [search existing issues](https://github.com/mcp-wp/ai-command/issues?q=label%3Abug%20) to see if theres an existing resolution to it, or if its already been fixed in a newer version.


Once youve done a bit of searching and discovered there isnt an open or fixed issue for your bug, please [create a new issue](https://github.com/swissspidy/ai-command/issues/new). Include as much detail as you can, and clear steps to reproduce if possible. For more guidance, [review our bug report documentation](https://make.wordpress.org/cli/handbook/bug-reports/). Once youve done a bit of searching and discovered there isnt an open or fixed issue for your bug, please [create a new issue](https://github.com/mcp-wp/ai-command/issues/new). Include as much detail as you can, and clear steps to reproduce if possible. For more guidance, [review our bug report documentation](https://make.wordpress.org/cli/handbook/bug-reports/).


### Creating a pull request ### Creating a pull request


Want to contribute a new feature? Please first [open a new issue](https://github.com/swissspidy/ai-command/issues/new) to discuss whether the feature is a good fit for the project. Want to contribute a new feature? Please first [open a new issue](https://github.com/mcp-wp/ai-command/issues/new) to discuss whether the feature is a good fit for the project.


Once you've decided to commit the time to seeing your pull request through, [please follow our guidelines for creating a pull request](https://make.wordpress.org/cli/handbook/pull-requests/) to make sure it's a pleasant experience. See "[Setting up](https://make.wordpress.org/cli/handbook/pull-requests/#setting-up)" for details specific to working on this package locally. Once you've decided to commit the time to seeing your pull request through, [please follow our guidelines for creating a pull request](https://make.wordpress.org/cli/handbook/pull-requests/) to make sure it's a pleasant experience. See "[Setting up](https://make.wordpress.org/cli/handbook/pull-requests/#setting-up)" for details specific to working on this package locally.



View file

@ -1,6 +1,6 @@
<?php <?php


namespace WP_CLI\AiCommand; namespace McpWp\AiCommand;


use WP_CLI; use WP_CLI;


@ -8,10 +8,12 @@ if ( ! class_exists( '\WP_CLI' ) ) {
return; return;
} }


$ai_command_autoloader = __DIR__ . '/vendor/autoload.php'; if ( file_exists( __DIR__ . '/third-party/vendor/autoload.php' ) ) {
require_once __DIR__ . '/third-party/vendor/autoload.php';
}


if ( file_exists( $ai_command_autoloader ) ) { if ( file_exists( __DIR__ . '/src/vendor/autoload.php' ) ) {
require_once $ai_command_autoloader; require_once __DIR__ . '/src/vendor/autoload.php';
} }


WP_CLI::add_command( 'ai', AiCommand::class ); WP_CLI::add_command( 'ai', AiCommand::class );

View file

@ -1,24 +1,42 @@
{ {
"name": "swissspidy/ai-command", "name": "mcp-wp/ai-command",
"type": "wp-cli-package", "type": "wp-cli-package",
"description": "", "description": "",
"homepage": "https://github.com/swissspidy/ai-command", "homepage": "https://github.com/mcp-wp/ai-command",
"license": "MIT", "license": "MIT",
"authors": [], "authors": [],
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"felixarntz/ai-services": "dev-main",
"logiscape/mcp-sdk-php": "^1.0", "logiscape/mcp-sdk-php": "^1.0",
"mcaskill/composer-exclude-files": "^4.0",
"mcp-wp/mcp-server": "dev-main",
"wp-cli/wp-cli": "^2.11" "wp-cli/wp-cli": "^2.11"
}, },
"require-dev": { "require-dev": {
"roave/security-advisories": "dev-latest", "humbug/php-scoper": "^0.18.17",
"wp-cli/wp-cli-tests": "^v4.3.9" "roave/security-advisories": "dev-latest",
"wp-cli/wp-cli-tests": "^v4.3.9"
}, },
"repositories": [
{
"type": "vcs",
"url": "https://github.com/mcp-wp/mcp-server",
"no-api": true
},
{
"type": "vcs",
"url": "https://github.com/felixarntz/ai-services",
"no-api": true
}
],
"config": { "config": {
"process-timeout": 7200, "process-timeout": 7200,
"sort-packages": true, "sort-packages": true,
"allow-plugins": { "allow-plugins": {
"composer/installers": true,
"dealerdirect/phpcodesniffer-composer-installer": true, "dealerdirect/phpcodesniffer-composer-installer": true,
"mcaskill/composer-exclude-files": true,
"php-http/discovery": true "php-http/discovery": true
} }
}, },
@ -32,12 +50,15 @@
"mcp server list", "mcp server list",
"mcp server add", "mcp server add",
"mcp server remove" "mcp server remove"
] ],
"exclude-from-files": [
],
"installer-disable": true
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"WP_CLI\\AiCommand\\": "src/", "McpWp\\AiCommand\\": "src/",
"WP_CLI\\AiCommand\\MCP\\": "src/MCP" "McpWp\\AiCommand\\MCP\\": "src/MCP"
}, },
"files": [ "files": [
"ai-command.php" "ai-command.php"
@ -46,6 +67,12 @@
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,
"scripts": { "scripts": {
"post-install-cmd": [
"@prefix-dependencies"
],
"post-update-cmd": [
"@prefix-dependencies"
],
"behat": "run-behat-tests", "behat": "run-behat-tests",
"behat-rerun": "rerun-behat-tests", "behat-rerun": "rerun-behat-tests",
"lint": "run-linter-tests", "lint": "run-linter-tests",
@ -58,9 +85,18 @@
"@phpcs", "@phpcs",
"@phpunit", "@phpunit",
"@behat" "@behat"
] ],
"prefix-dependencies": [
"php-scoper add-prefix --output-dir=./third-party --force --quiet",
"echo '{ \"autoload\": { \"classmap\": [\"\"] } }' > ./third-party/composer.json",
"@composer dump-autoload --working-dir ./third-party --no-dev --classmap-authoritative",
"sed -i'.bak' -e 's/Composer\\\\/AiCommand_Composer\\\\/' third-party/vendor/composer/*.php && rm -rf third-party/vendor/composer/*.php.bak",
"echo '{ \"autoload\": { \"classmap\": [\"\"] } }' > ./src/composer.json",
"@composer dump-autoload --working-dir ./src --no-dev --classmap-authoritative",
"sed -i'.bak' -e 's/Composer\\\\/AiCommand_Composer\\\\/' src/vendor/composer/*.php && rm -rf src/vendor/composer/*.php.bak"
]
}, },
"support": { "support": {
"issues": "https://github.com/swissspidy/ai-command/issues" "issues": "https://github.com/mcp-wp/ai-command/issues"
} }
} }

View file

@ -138,7 +138,7 @@ $server->register_resource(
List resources List resources


```PHP ```PHP
$server = new WP_CLI\AiCommand\MCP\Server(); $server = new McpWp\AiCommand\MCP\Server();
$resources = $server->list_resources(); $resources = $server->list_resources();


echo json_encode( $resources, JSON_PRETTY_PRINT ); echo json_encode( $resources, JSON_PRETTY_PRINT );
@ -147,7 +147,7 @@ echo json_encode( $resources, JSON_PRETTY_PRINT );
Read resource Read resource


```PHP ```PHP
$server = new WP_CLI\AiCommand\MCP\Server(); $server = new McpWp\AiCommand\MCP\Server();
$resource_data = $server->read_resource( 'file://./products.json' ); $resource_data = $server->read_resource( 'file://./products.json' );


echo json_encode( $resource_data, JSON_PRETTY_PRINT ); echo json_encode( $resource_data, JSON_PRETTY_PRINT );

View file

@ -45,10 +45,12 @@
<rule ref="WordPress.NamingConventions.PrefixAllGlobals"> <rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<properties> <properties>
<property name="prefixes" type="array"> <property name="prefixes" type="array">
<element value="WP_CLI\AiCommand"/><!-- Namespaces. --> <element value="McpWp\AiCommand"/><!-- Namespaces. -->
<element value="ai_command"/><!-- Global variables and such. --> <element value="ai_command"/><!-- Global variables and such. -->
</property> </property>
</properties> </properties>

<exclude-pattern>scoper.inc.php</exclude-pattern>
</rule> </rule>


<rule ref="WordPress.NamingConventions.ValidVariableName"> <rule ref="WordPress.NamingConventions.ValidVariableName">
@ -58,4 +60,9 @@
</property> </property>
</properties> </properties>
</rule> </rule>

<!-- Third-party or auto-generated code -->
<exclude-pattern>*/third-party/*</exclude-pattern>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/includes/vendor/*</exclude-pattern>
</ruleset> </ruleset>

17
scoper.inc.php Normal file
View file

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

use Symfony\Component\Finder\Finder;

return [
'prefix' => 'McpWp\AiCommand_Dependencies',
'finders' => [
Finder::create()
->files()
->in( 'vendor/logiscape/mcp-sdk-php/src' ),
Finder::create()
->files()
->in( 'vendor/mcp-wp/mcp-server/src/MCP' ),
],
'exclude-namespaces' => [ 'Psr' ],
'exclude-classes' => [ 'WP_Community_Events' ],
];

View file

@ -1,6 +1,6 @@
<?php <?php


namespace WP_CLI\AiCommand\AI; namespace McpWp\AiCommand\AI;


use Exception; use Exception;
use Felix_Arntz\AI_Services\Services\API\Enums\AI_Capability; use Felix_Arntz\AI_Services\Services\API\Enums\AI_Capability;

View file

@ -1,13 +1,13 @@
<?php <?php


namespace WP_CLI\AiCommand; namespace McpWp\AiCommand;


use Mcp\Client\ClientSession; use Mcp\Client\ClientSession;
use Mcp\Client\Transport\StdioServerParameters; use McpWp\AiCommand\AI\AiClient;
use WP_CLI\AiCommand\AI\AiClient; use McpWp\AiCommand\MCP\Client;
use WP_CLI\AiCommand\MCP\Client; use McpWp\AiCommand\Utils\CliLogger;
use WP_CLI\AiCommand\Utils\CliLogger; use McpWp\AiCommand\Utils\McpConfig;
use WP_CLI\AiCommand\Utils\McpConfig; use McpWp\AiCommand_Dependencies\McpWp\MCP\Servers\WordPress\WordPress;
use WP_CLI\Utils; use WP_CLI\Utils;
use WP_CLI_Command; use WP_CLI_Command;


@ -50,9 +50,6 @@ class AiCommand extends WP_CLI_Command {
$with_wordpress = null === Utils\get_flag_value( $assoc_args, 'skip-wordpress' ); $with_wordpress = null === Utils\get_flag_value( $assoc_args, 'skip-wordpress' );
if ( $with_wordpress ) { if ( $with_wordpress ) {
\WP_CLI::get_runner()->load_wordpress(); \WP_CLI::get_runner()->load_wordpress();
} else {
// TODO: Implement.
\WP_CLI::error( 'Not implemented yet' );
} }


$sessions = $this->get_sessions( $with_wordpress ); $sessions = $this->get_sessions( $with_wordpress );
@ -131,13 +128,13 @@ class AiCommand extends WP_CLI_Command {
$sessions = []; $sessions = [];


// The WP-CLI MCP server is always available. // The WP-CLI MCP server is always available.
$sessions[] = ( new MCP\Client( new CliLogger() ) )->connect( $sessions[] = ( new Client( new CliLogger() ) )->connect(
MCP\Servers\WP_CLI\WP_CLI::class MCP\Servers\WP_CLI\WP_CLI::class
); );


if ( $with_wordpress ) { if ( $with_wordpress ) {
$sessions[] = ( new Client( new CliLogger() ) )->connect( $sessions[] = ( new Client( new CliLogger() ) )->connect(
MCP\Servers\WordPress\WordPress::class WordPress::class
); );
} }



View file

@ -1,16 +1,14 @@
<?php <?php


namespace WP_CLI\AiCommand\MCP; namespace McpWp\AiCommand\MCP;


use Mcp\Client\Client as McpCLient; use McpWp\AiCommand_Dependencies\Mcp\Client\Client as McpCLient;
use Mcp\Client\ClientSession; use McpWp\AiCommand_Dependencies\Mcp\Client\ClientSession;
use Mcp\Client\Transport\StdioServerParameters; use McpWp\AiCommand_Dependencies\McpWp\MCP\Server;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;



class Client extends McpCLient { class Client extends McpCLient {
private ?ClientSession $session = null;


private LoggerInterface $logger; private LoggerInterface $logger;


@ -38,6 +36,7 @@ class Client extends McpCLient {
?array $env = null, ?array $env = null,
?float $read_timeout = null ?float $read_timeout = null
): ClientSession { ): ClientSession {
$session = null;
if ( class_exists( $command_or_url ) ) { if ( class_exists( $command_or_url ) ) {
/** /**
* @var Server $server * @var Server $server
@ -51,15 +50,15 @@ class Client extends McpCLient {


[$read_stream, $write_stream] = $transport->connect(); [$read_stream, $write_stream] = $transport->connect();


$this->session = new InMemorySession( $session = new InMemorySession(
$read_stream, $read_stream,
$write_stream, $write_stream,
$this->logger $this->logger
); );


$this->session->initialize(); $session->initialize();


return $this->session; return $session;
} }


// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
@ -82,17 +81,17 @@ class Client extends McpCLient {
[$read_stream, $write_stream] = $transport->connect(); [$read_stream, $write_stream] = $transport->connect();


// Initialize the client session with the obtained streams // Initialize the client session with the obtained streams
$this->session = new InMemorySession( $session = new InMemorySession(
$read_stream, $read_stream,
$write_stream, $write_stream,
$this->logger $this->logger
); );


// Initialize the session (e.g., perform handshake if necessary) // Initialize the session (e.g., perform handshake if necessary)
$this->session->initialize(); $session->initialize();
$this->logger->info( 'Session initialized successfully' ); $this->logger->info( 'Session initialized successfully' );


return $this->session; return $session;
} }


return parent::connect( $command_or_url, $args, $env, $read_timeout ); return parent::connect( $command_or_url, $args, $env, $read_timeout );

View file

@ -1,46 +1,20 @@
<?php <?php


/** namespace McpWp\AiCommand\MCP;
* Model Context Protocol SDK for PHP
*
* (c) 2024 Logiscape LLC <https://logiscape.com>
*
* Based on the Python SDK for the Model Context Protocol
* https://github.com/modelcontextprotocol/python-sdk
*
* PHP conversion developed by:
* - Josh Abbott
* - Claude 3.5 Sonnet (Anthropic AI model)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package logiscape/mcp-sdk-php
* @author Josh Abbott <https://joshabbott.com>
* @copyright Logiscape LLC
* @license MIT License
* @link https://github.com/logiscape/mcp-sdk-php
*
* Filename: Client/Transport/SseTransport.php
*/

declare(strict_types=1);

namespace WP_CLI\AiCommand\MCP;


use Exception; use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use Mcp\Shared\MemoryStream; use McpWp\AiCommand_Dependencies\Mcp\Shared\MemoryStream;
use Mcp\Types\JsonRpcMessage; use McpWp\AiCommand_Dependencies\Mcp\Types\JsonRpcMessage;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use RuntimeException; use RuntimeException;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Mcp\Types\JSONRPCRequest; use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCRequest;
use Mcp\Types\JSONRPCNotification; use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCNotification;
use Mcp\Types\JSONRPCResponse; use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCResponse;
use Mcp\Types\JSONRPCError; use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCError;
use Mcp\Types\RequestId; use McpWp\AiCommand_Dependencies\Mcp\Types\RequestId;
use Mcp\Types\JsonRpcErrorObject; use McpWp\AiCommand_Dependencies\Mcp\Types\JsonRpcErrorObject;
use WpOrg\Requests\Response; use WpOrg\Requests\Response;


/** /**

View file

@ -1,18 +1,18 @@
<?php <?php


namespace WP_CLI\AiCommand\MCP; namespace McpWp\AiCommand\MCP;


use InvalidArgumentException; use InvalidArgumentException;
use Mcp\Client\ClientSession; use McpWp\AiCommand_Dependencies\Mcp\Client\ClientSession;
use Mcp\Shared\ErrorData; use McpWp\AiCommand_Dependencies\Mcp\Shared\ErrorData;
use Mcp\Shared\McpError; use McpWp\AiCommand_Dependencies\Mcp\Shared\McpError;
use Mcp\Shared\MemoryStream; use McpWp\AiCommand_Dependencies\Mcp\Shared\MemoryStream;
use Mcp\Types\JSONRPCError; use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCError;
use Mcp\Types\JsonRpcMessage; use McpWp\AiCommand_Dependencies\Mcp\Types\JsonRpcMessage;
use Mcp\Types\JSONRPCRequest; use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCRequest;
use Mcp\Types\JSONRPCResponse; use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCResponse;
use Mcp\Types\McpModel; use McpWp\AiCommand_Dependencies\Mcp\Types\McpModel;
use Mcp\Types\RequestId; use McpWp\AiCommand_Dependencies\Mcp\Types\RequestId;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;



View file

@ -1,11 +1,12 @@
<?php <?php


namespace WP_CLI\AiCommand\MCP; namespace McpWp\AiCommand\MCP;


use Exception; use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use Mcp\Shared\MemoryStream; use McpWp\AiCommand_Dependencies\Mcp\Shared\MemoryStream;
use Mcp\Types\JsonRpcMessage; use McpWp\AiCommand_Dependencies\Mcp\Types\JsonRpcMessage;
use McpWp\AiCommand_Dependencies\McpWp\MCP\Server;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use RuntimeException; use RuntimeException;



View file

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

namespace WP_CLI\AiCommand\MCP;

use InvalidArgumentException;
use Mcp\Server\NotificationOptions;
use Mcp\Server\Server as McpServer;
use Mcp\Shared\ErrorData;
use Mcp\Shared\McpError;
use Mcp\Shared\Version;
use Mcp\Types\CallToolResult;
use Mcp\Types\Implementation;
use Mcp\Types\InitializeResult;
use Mcp\Types\JSONRPCError;
use Mcp\Types\JsonRpcErrorObject;
use Mcp\Types\JsonRpcMessage;
use Mcp\Types\JSONRPCNotification;
use Mcp\Types\JSONRPCRequest;
use Mcp\Types\JSONRPCResponse;
use Mcp\Types\ListResourcesResult;
use Mcp\Types\ListResourceTemplatesResult;
use Mcp\Types\ListToolsResult;
use Mcp\Types\ReadResourceResult;
use Mcp\Types\RequestId;
use Mcp\Types\RequestParams;
use Mcp\Types\Resource;
use Mcp\Types\ResourceTemplate;
use Mcp\Types\Result;
use Mcp\Types\TextContent;
use Mcp\Types\TextResourceContents;
use Mcp\Types\Tool;
use Mcp\Types\ToolInputSchema;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

class Server {
private array $data = [];

/**
* @var array<string, Tool>
*/
private array $tools = [];

/**
* @var Array<Resource>
*/
private array $resources = [];

/**
* @var Array<ResourceTemplate>
*/
private array $resource_templates = [];

protected McpServer $mcp_server;

protected LoggerInterface $logger;

public function __construct( private readonly string $name, ?LoggerInterface $logger = null ) {
$this->logger = $logger ?? new NullLogger();

$this->mcp_server = new McpServer( $name, $this->logger );

$this->mcp_server->registerHandler(
'initialize',
[ $this, 'initialize' ]
);

$this->mcp_server->registerHandler(
'tools/list',
[ $this, 'list_tools' ]
);

$this->mcp_server->registerHandler(
'tools/call',
[ $this, 'call_tool' ]
);

$this->mcp_server->registerHandler(
'resources/list',
[ $this, 'list_resources' ]
);

$this->mcp_server->registerHandler(
'resources/read',
[ $this, 'read_resources' ]
);

$this->mcp_server->registerHandler(
'resources/templates/list',
[ $this, 'list_resource_templates' ]
);

$this->mcp_server->registerNotificationHandler(
'notifications/initialized',
[ $this, 'do_nothing' ]
);
}

public function do_nothing(): void {
// Do nothing.
}

public function register_tool( array $tool_definition ): void {
if ( ! isset( $tool_definition['name'] ) || ! is_callable( $tool_definition['callable'] ) ) {
throw new InvalidArgumentException( "Invalid tool definition. Must be an array with 'name' and 'callable'." );
}

$name = $tool_definition['name'];
$callable = $tool_definition['callable'];
$description = $tool_definition['description'] ?? null;
$input_schema = $tool_definition['inputSchema'] ?? null;

$this->tools[ $name ] = [
'tool' => new Tool(
$name,
ToolInputSchema::fromArray(
$input_schema,
),
$description
),
'callable' => $callable,
'inputSchema' => $input_schema,
];
}

public function initialize(): InitializeResult {
return new InitializeResult(
capabilities: $this->mcp_server->getCapabilities( new NotificationOptions(), [] ),
serverInfo: new Implementation(
$this->name,
'0.0.1', // TODO: Make dynamic.
),
protocolVersion: Version::LATEST_PROTOCOL_VERSION
);
}

// TODO: Implement pagination, see https://spec.modelcontextprotocol.io/specification/2024-11-05/server/utilities/pagination/#response-format
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
public function list_tools( $params ): ListToolsResult {
$prepared_tools = [];
foreach ( $this->tools as $tool ) {
$prepared_tools[] = $tool['tool'];
}

return new ListToolsResult( $prepared_tools );
}

public function call_tool( $params ): CallToolResult {
$found_tool = null;
foreach ( $this->tools as $name => $tool ) {
if ( $name === $params->name ) {
$found_tool = $tool;
break;
}
}

if ( ! $found_tool ) {
throw new InvalidArgumentException( "Unknown tool: {$params->name}" );
}

$result = call_user_func( $found_tool['callable'], $params->arguments );

if ( $result instanceof CallToolResult ) {
return $result;
}

if ( is_wp_error( $result ) ) {
return new CallToolResult(
[
new TextContent(
$result->get_error_message()
),
],
true
);
}

if ( is_string( $result ) ) {
$result = [ new TextContent( $result ) ];
}

if ( ! is_array( $result ) ) {
$result = [ $result ];
}
return new CallToolResult( $result );
}

// TODO: Implement pagination, see https://spec.modelcontextprotocol.io/specification/2024-11-05/server/utilities/pagination/#response-format
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
public function list_resources(): ListResourcesResult {
return new ListResourcesResult( $this->resources );
}

// TODO: Make dynamic.
public function read_resources( RequestParams $params ): ReadResourceResult {
$uri = $params->uri;
if ( 'example://greeting' !== $uri ) {
throw new InvalidArgumentException( "Unknown resource: {$uri}" );
}

return new ReadResourceResult(
[
new TextResourceContents(
'Hello from the example MCP server!',
$uri,
'text/plain'
),
]
);
}

public function register_resource( Resource $res ): void {
$this->resources[ $res->name ] = $res;
}

// TODO: Implement pagination, see https://spec.modelcontextprotocol.io/specification/2024-11-05/server/utilities/pagination/#response-format
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
public function list_resource_templates( $params ): ListResourceTemplatesResult {
return new ListResourceTemplatesResult( $this->resource_templates );
}

public function register_resource_template( ResourceTemplate $resource_template ): void {
$this->resource_templates[ $resource_template->name ] = $resource_template;
}

/**
* Processes an incoming message from the client.
*/
public function handle_message( JsonRpcMessage $message ) {
$this->logger->debug( 'Received message: ' . json_encode( $message ) );

$inner_message = $message->message;

try {
if ( $inner_message instanceof JSONRPCRequest ) {
// It's a request
return $this->process_request( $inner_message );
}

if ( $inner_message instanceof JSONRPCNotification ) {
// It's a notification
$this->process_notification( $inner_message );
return null;
}

// Server does not expect responses from client; ignore or log
$this->logger->warning( 'Received unexpected message type: ' . get_class( $inner_message ) );
} catch ( McpError $e ) {
if ( $inner_message instanceof JSONRPCRequest ) {
return $this->send_error( $inner_message->id, $e->error );
}
} catch ( \Exception $e ) {
$this->logger->error( 'Error handling message: ' . $e->getMessage() );
if ( $inner_message instanceof JSONRPCRequest ) {
// Code -32603 is Internal error as per JSON-RPC spec
return $this->send_error(
$inner_message->id,
new ErrorData(
-32603,
$e->getMessage()
)
);
}
}
}

/**
* Processes a JSONRPCRequest message.
*/
private function process_request( JSONRPCRequest $request ): JsonRpcMessage {
$method = $request->method;
$handlers = $this->mcp_server->getHandlers();
$handler = $handlers[ $method ] ?? null;

if ( null === $handler ) {
throw new McpError(
new ErrorData(
-32601, // Method not found
"Method not found: {$method}"
)
);
}

$params = $request->params ?? null;
$result = $handler( $params );

if ( ! $result instanceof Result ) {
$result = new Result();
}

return $this->send_response( $request->id, $result );
}

/**
* Processes a JSONRPCNotification message.
*/
private function process_notification( JSONRPCNotification $notification ): void {
$method = $notification->method;
$handlers = $this->mcp_server->getNotificationHandlers();
$handler = $handlers[ $method ] ?? null;

if ( null !== $handler ) {
$params = $notification->params ?? null;
$handler( $params );
}

$this->logger->warning( "No handler registered for notification method: $method" );
}

/**
* Sends a response to a request.
*
* @param RequestId $id The request ID to respond to.
* @param Result $result The result object.
*/
private function send_response( RequestId $id, Result $result ): JsonRpcMessage {
// Create a JSONRPCResponse object and wrap in JsonRpcMessage
$response = new JSONRPCResponse(
'2.0',
$id,
$result
);
$response->validate();

return new JsonRpcMessage( $response );
}


/**
* Sends an error response to a request.
*
* @param RequestId $id The request ID to respond to.
* @param ErrorData $error The error data.
*/
private function send_error( RequestId $id, ErrorData $error ): JsonRpcMessage {
$error_object = new JsonRpcErrorObject(
$error->code,
$error->message,
$error->data ?? null
);

$response = new JSONRPCError(
'2.0',
$id,
$error_object
);
$response->validate();

return new JsonRpcMessage( $response );
}
}

View file

@ -1,6 +1,6 @@
<?php <?php


namespace WP_CLI\AiCommand\MCP\Servers\WP_CLI\Tools; namespace McpWp\AiCommand\MCP\Servers\WP_CLI\Tools;


use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WP_CLI; use WP_CLI;

View file

@ -1,9 +1,9 @@
<?php <?php


namespace WP_CLI\AiCommand\MCP\Servers\WP_CLI; namespace McpWp\AiCommand\MCP\Servers\WP_CLI;


use WP_CLI\AiCommand\MCP\Server; use McpWp\AiCommand_Dependencies\McpWp\MCP\Server;
use WP_CLI\AiCommand\MCP\Servers\WP_CLI\Tools\CliCommands; use McpWp\AiCommand\MCP\Servers\WP_CLI\Tools\CliCommands;


class WP_CLI extends Server { class WP_CLI extends Server {
public function __construct() { public function __construct() {

View file

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

namespace WP_CLI\AiCommand\MCP\Servers\WordPress;

class MediaManager {

public static function upload_to_media_library( $media_path ) {
// Get WordPress upload directory information
$upload_dir = wp_upload_dir();

// Get the file name from the path
$file_name = basename( $media_path );

// Copy file to the upload directory
$new_file_path = $upload_dir['path'] . '/' . $file_name;
copy( $media_path, $new_file_path );

// Prepare attachment data
$wp_filetype = wp_check_filetype( $file_name, null );
$attachment = array(
'post_mime_type' => $wp_filetype['type'],
'post_title' => sanitize_file_name( $file_name ),
'post_content' => '',
'post_status' => 'inherit',
);

// Insert the attachment
$attach_id = wp_insert_attachment( $attachment, $new_file_path );

// Generate attachment metadata
require_once ABSPATH . 'wp-admin/includes/image.php';
$attach_data = wp_generate_attachment_metadata( $attach_id, $new_file_path );
wp_update_attachment_metadata( $attach_id, $attach_data );

return $attach_id;
}
}

View file

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

namespace WP_CLI\AiCommand\MCP\Servers\WordPress\Tools;

use Mcp\Types\TextContent;
use WP_Community_Events;

readonly class CommunityEvents {

public function get_tools(): array {
$tools = [];

$tools[] = [
'name' => 'fetch_wp_community_events',
'description' => 'Fetches upcoming WordPress community events near a specified city or the user\'s current location. If no events are found in the exact location, nearby events within a specific radius will be considered.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'location' => [
'type' => 'string',
'description' => 'City name or "near me" for auto-detected location. If no events are found in the exact location, the tool will also consider nearby events within a specified radius (default: 100 km).',
],
],
'required' => [ 'location' ], // We only require the location
],
'callable' => static function ( $params ) {
$location_input = strtolower( trim( $params['location'] ) );

// Manually include the WP_Community_Events class if it's not loaded
if ( ! class_exists( 'WP_Community_Events' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php';
}

$location = [
'description' => $location_input,
];

$events_instance = new WP_Community_Events( 0, $location );

// Get events from WP_Community_Events
$events = $events_instance->get_events( $location_input );

// Check for WP_Error
if ( is_wp_error( $events ) ) {
return $events;
}

return new TextContent(
json_encode( $events['events'] )
);
},
];

return $tools;
}
}

View file

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

namespace WP_CLI\AiCommand\MCP\Servers\WordPress\Tools;

use Mcp\Types\TextContent;

readonly class Dummy {

public function get_tools(): array {
$tools = [];

$tools[] = [
'name' => 'greet-user',
'description' => 'Greet a given user by their name',
'inputSchema' => [
'type' => 'object',
'properties' => [
'name' => [
'type' => 'string',
'description' => 'Name',
],
],
'required' => [ 'name' ],
],
'callable' => static function ( $arguments ) {
$name = $arguments['name'];

return new TextContent(
"Hello my friend, $name"
);
},
];

return $tools;
}
}

View file

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

namespace WP_CLI\AiCommand\MCP\Servers\WordPress\Tools;

use Mcp\Types\TextContent;
use WP_CLI\AiCommand\MCP\Servers\WordPress\WpAiClient;

readonly class ImageTools {
public function get_tools(): array {
$tools = [];

$tools[] = [
'name' => 'generate_image',
'description' => 'Generates an image.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'prompt' => [
'type' => 'string',
'description' => 'The prompt for generating the image.',
],
],
'required' => [ 'prompt' ],
],
'callable' => function ( $params ) {
$client = new WpAiClient();

return new TextContent(
$client->get_image_from_ai_service( $params['prompt'] )
);
},
];

$tools[] = [
'name' => 'modify_image',
'description' => 'Modifies an image with a given image id and prompt.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'prompt' => [
'type' => 'string',
'description' => 'The prompt for generating the image.',
],
'media_id' => [
'type' => 'string',
'description' => 'the id of the media element',
],
],
'required' => [ 'prompt', 'media_id' ],
],
'callable' => function ( $params ) {
$media_element = [
'filepath' => get_attached_file( $params['media_id'] ),
'mime_type' => get_post_mime_type( $params['media_id'] ),
];

$client = new WpAiClient();

return new TextContent(
$client->modify_image_with_ai( $params['prompt'], $media_element )
);
},
];

return $tools;
}
}

View file

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

namespace WP_CLI\AiCommand\MCP\Servers\WordPress\Tools;

use Psr\Log\LoggerInterface;
use WP_REST_Request;
use WP_REST_Response;

readonly class RestApi {
public function __construct( private LoggerInterface $logger ) {
}

private function args_to_schema( $args = [] ) {
$schema = [];
$required = [];

if ( empty( $args ) ) {
return [];
}

foreach ( $args as $title => $arg ) {
$description = $arg['description'] ?? $title;
$type = $this->sanitize_type( $arg['type'] ?? 'string' );

$schema[ $title ] = [
'type' => $type,
'description' => $description,
];
if ( isset( $arg['required'] ) && $arg['required'] ) {
$required[] = $title;
}
}

return [
'type' => 'object',
'properties' => $schema,
'required' => $required,
];
}

protected function sanitize_type( $type ) {

$mapping = array(
'string' => 'string',
'integer' => 'integer',
'number' => 'integer',
'boolean' => 'boolean',
);

// Validated types:
if ( ! \is_array( $type ) && isset( $mapping[ $type ] ) ) {
return $mapping[ $type ];
}

if ( 'array' === $type || 'object' === $type ) {
return 'string'; // TODO, better solution.
}
if ( empty( $type ) || 'null' === $type ) {
return 'string';
}

if ( ! \is_array( $type ) ) {
throw new \Exception( 'Invalid type: ' . $type );
}

// Find valid values in array.
if ( \in_array( 'string', $type, true ) ) {
return 'string';
}
if ( \in_array( 'integer', $type, true ) ) {
return 'integer';
}
// TODO, better types handling.
return 'string';
}

public function get_tools(): array {
$server = rest_get_server();
$routes = $server->get_routes();
$tools = [];

foreach ( $routes as $route => $endpoints ) {
foreach ( $endpoints as $endpoint ) {
foreach ( $endpoint['methods'] as $method_name => $enabled ) {
$information = new RouteInformation(
$route,
$method_name,
$endpoint['callback'],
);

if ( ! $information->is_wp_rest_controller() ) {
continue;
}

$tool = [
'name' => $information->get_sanitized_route_name(),
'description' => $this->generate_description( $information ),
'inputSchema' => $this->args_to_schema( $endpoint['args'] ),
'callable' => function ( $inputs ) use ( $route, $method_name, $server ) {
return json_encode( $this->rest_callable( $inputs, $route, $method_name, $server ) );
},
];

$tools[] = $tool;
}
}
}

return $tools;
}

/**
* Create description based on route and method.
*
* Get a list of posts GET /wp/v2/posts
* Get post with id GET /wp/v2/posts/(?P<id>[\d]+)
*/
protected function generate_description( RouteInformation $information ): string {

$verb = match ( $information->get_method() ) {
'GET' => 'Get',
'POST' => 'Create',
'PUT', 'PATCH' => 'Update',
'DELETE' => 'Delete',
};

$schema = $information->get_wp_rest_controller()->get_public_item_schema();
$title = $schema['title'];

$determiner = $information->is_singular()
? 'a'
: 'list of';

return $verb . ' ' . $determiner . ' ' . $title;
}

protected function rest_callable( $inputs, $route, $method_name, \WP_REST_Server $server ): array {
preg_match_all( '/\(?P<(\w+)>/', $route, $matches );

foreach ( $matches[1] as $match ) {
if ( array_key_exists( $match, $inputs ) ) {
$route = preg_replace( '/(\(\?P<' . $match . '>.*?\))/', $inputs[ $match ], $route, 1 );
}
}

$this->logger->debug( 'Rest Route: ' . $route . ' ' . $method_name );

if ( isset( $inputs['meta'] ) ) {
if ( false === $inputs['meta'] || '' === $inputs['meta'] || [] === $inputs['meta'] ) {
unset( $inputs['meta'] );
}
}

foreach ( $inputs as $key => $value ) {
$this->logger->debug( ' param->' . $key . ' : ' . $value );
}

$request = new WP_REST_Request( $method_name, $route );
$request->set_body_params( $inputs );

/**
* @var WP_REST_Response $response
*/
$response = $server->dispatch( $request );

$data = $server->response_to_data( $response, false );

// Quick fix to reduce amount of data that is returned.
// TODO: Improve
unset( $data['_links'], $data[0]['_links'] );

if ( isset( $data[0]['slug'] ) ) {
$debug_data = 'Result List: ';
foreach ( $data as $item ) {
$debug_data .= $item['id'] . '=>' . $item['slug'] . ', ';
}
} elseif ( isset( $data['slug'] ) ) {
$debug_data = 'Result: ' . $data['id'] . ' ' . $data['slug'];
} else {
$debug_data = 'Unknown format';
}

$this->logger->debug( $debug_data );

return $data;
}
}

View file

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

declare(strict_types=1);

namespace WP_CLI\AiCommand\MCP\Servers\WordPress\Tools;

use BadMethodCallException;
use WP_REST_Controller;
use WP_REST_Posts_Controller;
use WP_REST_Taxonomies_Controller;
use WP_REST_Users_Controller;

readonly class RouteInformation {

public function __construct(
private string $route,
private string $method,
private mixed $callback,
) {
}

public function get_sanitized_route_name(): string {
$route = $this->route;

preg_match_all( '/\(?P<(\w+)>/', $this->route, $matches );

foreach ( $matches[1] as $match ) {
$route = preg_replace( '/(\(\?P<' . $match . '>.*\))/', 'p_' . $match, $route, 1 );
}

return $this->method . '_' . sanitize_title( $route );
}

public function get_route(): string {
return $this->route;
}

public function get_method(): string {
return $this->method;
}

public function is_create(): bool {
return 'POST' === $this->method;
}

public function is_update(): bool {
return 'PUT' === $this->method || 'PATCH' === $this->method;
}

public function is_delete(): bool {
return 'DELETE' === $this->method;
}

public function is_get(): bool {
return 'GET' === $this->method;
}

public function is_singular(): bool {
// Always true
if ( str_ends_with( $this->route, '(?P<id>[\d]+)' ) ) {
return true;
}

// Never true
if ( ! str_contains( $this->route, '?P<id>' ) ) {
return false;
}

return false;
}

public function is_wp_rest_controller(): bool {
// The callback form for a WP_REST_Controller is [ WP_REST_Controller, method ]
if ( ! is_array( $this->callback ) ) {
return false;
}

$allowed = [
WP_REST_Posts_Controller::class,
WP_REST_Users_Controller::class,
WP_REST_Taxonomies_Controller::class,
];

/**
* Filters the list of supported REST API controllers in the WordPress MCP server.
*
* @param array<class-string> $allowed List of REST API controller class names.
*/
$allowed = apply_filters( 'ai_command_wordpress_allowed_rest_controllers', $allowed );

foreach ( $allowed as $controller ) {
if ( $this->callback[0] instanceof $controller ) {
return true;
}
}

return false;
}

public function get_wp_rest_controller(): WP_REST_Controller {
if ( ! $this->is_wp_rest_controller() ) {
throw new BadMethodCallException( 'The callback needs to be a WP_Rest_Controller' );
}

return $this->callback[0];
}
}

View file

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

namespace WP_CLI\AiCommand\MCP\Servers\WordPress;

use Mcp\Types\Resource;
use Mcp\Types\ResourceTemplate;
use WP_CLI\AiCommand\MCP\Server;
use WP_CLI\AiCommand\MCP\Servers\WordPress\Tools\CommunityEvents;
use WP_CLI\AiCommand\MCP\Servers\WordPress\Tools\Dummy;
use WP_CLI\AiCommand\MCP\Servers\WordPress\Tools\ImageTools;
use WP_CLI\AiCommand\MCP\Servers\WordPress\Tools\RestApi;

class WordPress extends Server {
public function __construct() {
parent::__construct( 'WordPress' );

$all_tools = [
...( new RestApi( $this->logger ) )->get_tools(),
...( new CommunityEvents() )->get_tools(),
...( new Dummy() )->get_tools(),
...( new ImageTools() )->get_tools(),
];

/**
* Filters all the tools exposed by the WordPress MCP server.
*
* @param array $all_tools MCP tools.
*/
$all_tools = apply_filters( 'ai_command_wordpress_tools', $all_tools );

foreach ( $all_tools as $tool ) {
$this->register_tool( $tool );
}

/**
* Fires after tools have been registered in the WordPress MCP server.
*
* Can be used to register additional tools.
*
* @param Server $server WordPress MCP server instance.
*/
do_action( 'ai_command_wordpress_tools_loaded', $this );

$this->register_resource(
new Resource(
'Greeting Text',
'example://greeting',
'A simple greeting message',
'text/plain'
)
);

$this->register_resource_template(
new ResourceTemplate(
'Attachment',
'media://{id}',
'WordPress attachment',
'application/octet-stream'
)
);
}
}

View file

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

namespace WP_CLI\AiCommand\MCP\Servers\WordPress;

use Felix_Arntz\AI_Services\Services\API\Enums\AI_Capability;
use Felix_Arntz\AI_Services\Services\API\Helpers;
use Felix_Arntz\AI_Services\Services\API\Types\Parts\File_Data_Part;
use Felix_Arntz\AI_Services\Services\API\Types\Parts\Inline_Data_Part;
use RuntimeException;

class WpAiClient {
// Must not have the same name as the tool, otherwise it takes precedence.
public function get_image_from_ai_service( string $prompt ) {
// See https://github.com/felixarntz/ai-services/issues/25.
add_filter(
'map_meta_cap',
static function () {
return [ 'exist' ];
}
);

$service = ai_services()->get_available_service(
[
'capabilities' => [
AI_Capability::IMAGE_GENERATION,
],
]
);
$candidates = $service
->get_model(
[
'feature' => 'image-generation',
'capabilities' => [
AI_Capability::IMAGE_GENERATION,
],
]
)
->generate_image( $prompt );

$image_id = null;
foreach ( $candidates->get( 0 )->get_content()->get_parts() as $part ) {
if ( $part instanceof Inline_Data_Part ) {
$image_url = $part->get_base64_data(); // Data URL.
$image_blob = Helpers::base64_data_url_to_blob( $image_url );

if ( $image_blob ) {
$filename = tempnam( '/tmp', 'ai-generated-image' );
$parts = explode( '/', $part->get_mime_type() );
$extension = $parts[1];
rename( $filename, $filename . '.' . $extension );
$filename .= '.' . $extension;

file_put_contents( $filename, $image_blob->get_binary_data() );

$image_url = $filename;
$image_id = MediaManager::upload_to_media_library( $image_url );
}

break;
}

if ( $part instanceof File_Data_Part ) {
$image_url = $part->get_file_uri(); // Actual URL. May have limited TTL (often 1 hour).
// TODO: Save as file or so.
break;
}
}

return $image_id ?: 'no image found';
}

// Temporary workaround until AI Services plugin supports it.
public function modify_image_with_ai( $prompt, $media_element ): bool {

$mime_type = $media_element['mime_type'];
$image_path = $media_element['filepath'];
$image_contents = file_get_contents( $image_path );

// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
$base64_image = base64_encode( $image_contents );

// API Configuration
$api_key = get_option( 'ais_google_api_key' );

if ( ! $api_key ) {
throw new RuntimeException( 'Gemini API Key is not available' );
}
$model = 'gemini-2.0-flash-exp';
$api_url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$api_key}";

// Prepare request payload
$payload = [
'contents' => [
[
'role' => 'user',
'parts' => [
[
'text' => $prompt,
],
[
'inline_data' => [
'mime_type' => $mime_type,
'data' => $base64_image,
],
],
],
],
],
'generationConfig' => [
'responseModalities' => [ 'TEXT', 'IMAGE' ],
],
];

// Convert payload to JSON
$json_payload = json_encode( $payload );

// Set up cURL request
$ch = curl_init( $api_url );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_POST, true );
curl_setopt( $ch, CURLOPT_POSTFIELDS, $json_payload );
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Content-Length: ' . strlen( $json_payload ),
]
);

// Execute request
$response = curl_exec( $ch );
$error = curl_error( $ch );
$status_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );

// Handle errors
if ( $error ) {
throw new RuntimeException( 'cURL Error: ' . $error );
}

if ( $status_code >= 400 ) {
throw new RuntimeException( "API Error (Status $status_code): " . $response );
}

// Process response
$response_data = json_decode( $response, true );

// Check for valid response
if ( empty( $response_data ) || ! isset( $response_data['candidates'][0]['content']['parts'] ) ) {
throw new RuntimeException( 'Invalid API response format' );
}

// Extract image data from response
$image_data = null;
foreach ( $response_data['candidates'][0]['content']['parts'] as $part ) {
if ( isset( $part['inlineData'] ) ) {
$image_data = $part['inlineData']['data'];
$response_mime_type = $part['inlineData']['mimeType'];
break;
}
}

if ( ! $image_data ) {
throw new RuntimeException( 'No image data in response' );
}

// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$binary_data = base64_decode( $image_data );
if ( false === $binary_data ) {
throw new RuntimeException( 'Failed to decode image data' );
}

// Create temporary file for the image
$extension = explode( '/', $response_mime_type )[1] ?? 'jpg';
$filename = tempnam( '/tmp', 'ai-generated-image' );
rename( $filename, $filename . '.' . $extension );
$filename .= '.' . $extension;

// Save image to the file
if ( ! file_put_contents( $filename, $binary_data ) ) {
throw new RuntimeException( 'Failed to save image to temporary file' );
}

// Upload to media library
$image_id = MediaManager::upload_to_media_library( $filename );

if ( $image_id ) {
return $image_id;
}

return false;
}
}

View file

@ -1,8 +1,8 @@
<?php <?php


namespace WP_CLI\AiCommand; namespace McpWp\AiCommand;


use WP_CLI\AiCommand\Utils\McpConfig; use McpWp\AiCommand\Utils\McpConfig;
use WP_CLI\Formatter; use WP_CLI\Formatter;
use WP_CLI_Command; use WP_CLI_Command;



View file

@ -1,6 +1,6 @@
<?php <?php


namespace WP_CLI\AiCommand\Utils; namespace McpWp\AiCommand\Utils;


use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use InvalidArgumentException; use InvalidArgumentException;

View file

@ -1,6 +1,6 @@
<?php <?php


namespace WP_CLI\AiCommand\Utils; namespace McpWp\AiCommand\Utils;


use WP_CLI\Utils; use WP_CLI\Utils;
use WP_CLI_Command; use WP_CLI_Command;

View file

@ -1,6 +1,6 @@
<?php <?php


namespace WP_CLI\AiCommand\Tests\MCP\Client; namespace McpWp\AiCommand\Tests\MCP\Client;


use WP_CLI\Tests\TestCase; use WP_CLI\Tests\TestCase;