mirror of
https://hk.gh-proxy.com/https://github.com/mcp-wp/ai-command.git
synced 2025-10-03 10:10:57 +08:00
Import code from mcp-server / de-duplication (#51)
* Import mcp-server repo with php-scoper * exclude folder
This commit is contained in:
parent
feed34e25e
commit
37572bd180
29 changed files with 140 additions and 1201 deletions
2
.github/workflows/code-quality.yml
vendored
2
.github/workflows/code-quality.yml
vendored
|
@ -53,7 +53,7 @@ jobs:
|
|||
custom-cache-suffix: $(date -u "+%Y-%m")
|
||||
|
||||
- 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:
|
||||
name: PHPCS
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,6 +2,9 @@
|
|||
wp-cli.local.yml
|
||||
node_modules/
|
||||
vendor/
|
||||
src/vendor/
|
||||
src/composer.json
|
||||
third-party/
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.swp
|
||||
|
|
|
@ -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:
|
||||
|
||||
```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.
|
||||
|
@ -165,13 +165,13 @@ For a more thorough introduction, [check out WP-CLI's guide to contributing](htt
|
|||
|
||||
Think you’ve found a bug? We’d 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 there’s an existing resolution to it, or if it’s 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 there’s an existing resolution to it, or if it’s already been fixed in a newer version.
|
||||
|
||||
Once you’ve done a bit of searching and discovered there isn’t 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 you’ve done a bit of searching and discovered there isn’t 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
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand;
|
||||
namespace McpWp\AiCommand;
|
||||
|
||||
use WP_CLI;
|
||||
|
||||
|
@ -8,10 +8,12 @@ if ( ! class_exists( '\WP_CLI' ) ) {
|
|||
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 ) ) {
|
||||
require_once $ai_command_autoloader;
|
||||
if ( file_exists( __DIR__ . '/src/vendor/autoload.php' ) ) {
|
||||
require_once __DIR__ . '/src/vendor/autoload.php';
|
||||
}
|
||||
|
||||
WP_CLI::add_command( 'ai', AiCommand::class );
|
||||
|
|
|
@ -1,24 +1,42 @@
|
|||
{
|
||||
"name": "swissspidy/ai-command",
|
||||
"name": "mcp-wp/ai-command",
|
||||
"type": "wp-cli-package",
|
||||
"description": "",
|
||||
"homepage": "https://github.com/swissspidy/ai-command",
|
||||
"homepage": "https://github.com/mcp-wp/ai-command",
|
||||
"license": "MIT",
|
||||
"authors": [],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"felixarntz/ai-services": "dev-main",
|
||||
"logiscape/mcp-sdk-php": "^1.0",
|
||||
"mcaskill/composer-exclude-files": "^4.0",
|
||||
"mcp-wp/mcp-server": "dev-main",
|
||||
"wp-cli/wp-cli": "^2.11"
|
||||
},
|
||||
"require-dev": {
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"wp-cli/wp-cli-tests": "^v4.3.9"
|
||||
"humbug/php-scoper": "^0.18.17",
|
||||
"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": {
|
||||
"process-timeout": 7200,
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"composer/installers": true,
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||
"mcaskill/composer-exclude-files": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
|
@ -32,12 +50,15 @@
|
|||
"mcp server list",
|
||||
"mcp server add",
|
||||
"mcp server remove"
|
||||
]
|
||||
],
|
||||
"exclude-from-files": [
|
||||
],
|
||||
"installer-disable": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WP_CLI\\AiCommand\\": "src/",
|
||||
"WP_CLI\\AiCommand\\MCP\\": "src/MCP"
|
||||
"McpWp\\AiCommand\\": "src/",
|
||||
"McpWp\\AiCommand\\MCP\\": "src/MCP"
|
||||
},
|
||||
"files": [
|
||||
"ai-command.php"
|
||||
|
@ -46,6 +67,12 @@
|
|||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
"@prefix-dependencies"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@prefix-dependencies"
|
||||
],
|
||||
"behat": "run-behat-tests",
|
||||
"behat-rerun": "rerun-behat-tests",
|
||||
"lint": "run-linter-tests",
|
||||
|
@ -58,9 +85,18 @@
|
|||
"@phpcs",
|
||||
"@phpunit",
|
||||
"@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": {
|
||||
"issues": "https://github.com/swissspidy/ai-command/issues"
|
||||
"issues": "https://github.com/mcp-wp/ai-command/issues"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -138,7 +138,7 @@ $server->register_resource(
|
|||
List resources
|
||||
|
||||
```PHP
|
||||
$server = new WP_CLI\AiCommand\MCP\Server();
|
||||
$server = new McpWp\AiCommand\MCP\Server();
|
||||
$resources = $server->list_resources();
|
||||
|
||||
echo json_encode( $resources, JSON_PRETTY_PRINT );
|
||||
|
@ -147,7 +147,7 @@ echo json_encode( $resources, JSON_PRETTY_PRINT );
|
|||
Read resource
|
||||
|
||||
```PHP
|
||||
$server = new WP_CLI\AiCommand\MCP\Server();
|
||||
$server = new McpWp\AiCommand\MCP\Server();
|
||||
$resource_data = $server->read_resource( 'file://./products.json' );
|
||||
|
||||
echo json_encode( $resource_data, JSON_PRETTY_PRINT );
|
||||
|
|
|
@ -45,10 +45,12 @@
|
|||
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
|
||||
<properties>
|
||||
<property name="prefixes" type="array">
|
||||
<element value="WP_CLI\AiCommand"/><!-- Namespaces. -->
|
||||
<element value="McpWp\AiCommand"/><!-- Namespaces. -->
|
||||
<element value="ai_command"/><!-- Global variables and such. -->
|
||||
</property>
|
||||
</properties>
|
||||
|
||||
<exclude-pattern>scoper.inc.php</exclude-pattern>
|
||||
</rule>
|
||||
|
||||
<rule ref="WordPress.NamingConventions.ValidVariableName">
|
||||
|
@ -58,4 +60,9 @@
|
|||
</property>
|
||||
</properties>
|
||||
</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>
|
||||
|
|
17
scoper.inc.php
Normal file
17
scoper.inc.php
Normal 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' ],
|
||||
];
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\AI;
|
||||
namespace McpWp\AiCommand\AI;
|
||||
|
||||
use Exception;
|
||||
use Felix_Arntz\AI_Services\Services\API\Enums\AI_Capability;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand;
|
||||
namespace McpWp\AiCommand;
|
||||
|
||||
use Mcp\Client\ClientSession;
|
||||
use Mcp\Client\Transport\StdioServerParameters;
|
||||
use WP_CLI\AiCommand\AI\AiClient;
|
||||
use WP_CLI\AiCommand\MCP\Client;
|
||||
use WP_CLI\AiCommand\Utils\CliLogger;
|
||||
use WP_CLI\AiCommand\Utils\McpConfig;
|
||||
use McpWp\AiCommand\AI\AiClient;
|
||||
use McpWp\AiCommand\MCP\Client;
|
||||
use McpWp\AiCommand\Utils\CliLogger;
|
||||
use McpWp\AiCommand\Utils\McpConfig;
|
||||
use McpWp\AiCommand_Dependencies\McpWp\MCP\Servers\WordPress\WordPress;
|
||||
use WP_CLI\Utils;
|
||||
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' );
|
||||
if ( $with_wordpress ) {
|
||||
\WP_CLI::get_runner()->load_wordpress();
|
||||
} else {
|
||||
// TODO: Implement.
|
||||
\WP_CLI::error( 'Not implemented yet' );
|
||||
}
|
||||
|
||||
$sessions = $this->get_sessions( $with_wordpress );
|
||||
|
@ -131,13 +128,13 @@ class AiCommand extends WP_CLI_Command {
|
|||
$sessions = [];
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
if ( $with_wordpress ) {
|
||||
$sessions[] = ( new Client( new CliLogger() ) )->connect(
|
||||
MCP\Servers\WordPress\WordPress::class
|
||||
WordPress::class
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\MCP;
|
||||
namespace McpWp\AiCommand\MCP;
|
||||
|
||||
use Mcp\Client\Client as McpCLient;
|
||||
use Mcp\Client\ClientSession;
|
||||
use Mcp\Client\Transport\StdioServerParameters;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Client\Client as McpCLient;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Client\ClientSession;
|
||||
use McpWp\AiCommand_Dependencies\McpWp\MCP\Server;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
|
||||
class Client extends McpCLient {
|
||||
private ?ClientSession $session = null;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
|
@ -38,6 +36,7 @@ class Client extends McpCLient {
|
|||
?array $env = null,
|
||||
?float $read_timeout = null
|
||||
): ClientSession {
|
||||
$session = null;
|
||||
if ( class_exists( $command_or_url ) ) {
|
||||
/**
|
||||
* @var Server $server
|
||||
|
@ -51,15 +50,15 @@ class Client extends McpCLient {
|
|||
|
||||
[$read_stream, $write_stream] = $transport->connect();
|
||||
|
||||
$this->session = new InMemorySession(
|
||||
$session = new InMemorySession(
|
||||
$read_stream,
|
||||
$write_stream,
|
||||
$this->logger
|
||||
);
|
||||
|
||||
$this->session->initialize();
|
||||
$session->initialize();
|
||||
|
||||
return $this->session;
|
||||
return $session;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
|
||||
|
@ -82,17 +81,17 @@ class Client extends McpCLient {
|
|||
[$read_stream, $write_stream] = $transport->connect();
|
||||
|
||||
// Initialize the client session with the obtained streams
|
||||
$this->session = new InMemorySession(
|
||||
$session = new InMemorySession(
|
||||
$read_stream,
|
||||
$write_stream,
|
||||
$this->logger
|
||||
);
|
||||
|
||||
// Initialize the session (e.g., perform handshake if necessary)
|
||||
$this->session->initialize();
|
||||
$session->initialize();
|
||||
$this->logger->info( 'Session initialized successfully' );
|
||||
|
||||
return $this->session;
|
||||
return $session;
|
||||
}
|
||||
|
||||
return parent::connect( $command_or_url, $args, $env, $read_timeout );
|
||||
|
|
|
@ -1,46 +1,20 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* 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;
|
||||
namespace McpWp\AiCommand\MCP;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Shared\MemoryStream;
|
||||
use Mcp\Types\JsonRpcMessage;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Shared\MemoryStream;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\JsonRpcMessage;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Psr\Log\NullLogger;
|
||||
use Mcp\Types\JSONRPCRequest;
|
||||
use Mcp\Types\JSONRPCNotification;
|
||||
use Mcp\Types\JSONRPCResponse;
|
||||
use Mcp\Types\JSONRPCError;
|
||||
use Mcp\Types\RequestId;
|
||||
use Mcp\Types\JsonRpcErrorObject;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCRequest;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCNotification;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCResponse;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCError;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\RequestId;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\JsonRpcErrorObject;
|
||||
use WpOrg\Requests\Response;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\MCP;
|
||||
namespace McpWp\AiCommand\MCP;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Client\ClientSession;
|
||||
use Mcp\Shared\ErrorData;
|
||||
use Mcp\Shared\McpError;
|
||||
use Mcp\Shared\MemoryStream;
|
||||
use Mcp\Types\JSONRPCError;
|
||||
use Mcp\Types\JsonRpcMessage;
|
||||
use Mcp\Types\JSONRPCRequest;
|
||||
use Mcp\Types\JSONRPCResponse;
|
||||
use Mcp\Types\McpModel;
|
||||
use Mcp\Types\RequestId;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Client\ClientSession;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Shared\ErrorData;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Shared\McpError;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Shared\MemoryStream;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCError;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\JsonRpcMessage;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCRequest;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\JSONRPCResponse;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\McpModel;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\RequestId;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\MCP;
|
||||
namespace McpWp\AiCommand\MCP;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Shared\MemoryStream;
|
||||
use Mcp\Types\JsonRpcMessage;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Shared\MemoryStream;
|
||||
use McpWp\AiCommand_Dependencies\Mcp\Types\JsonRpcMessage;
|
||||
use McpWp\AiCommand_Dependencies\McpWp\MCP\Server;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\MCP\Servers\WP_CLI\Tools;
|
||||
namespace McpWp\AiCommand\MCP\Servers\WP_CLI\Tools;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WP_CLI;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\MCP\Servers\WP_CLI;
|
||||
namespace McpWp\AiCommand\MCP\Servers\WP_CLI;
|
||||
|
||||
use WP_CLI\AiCommand\MCP\Server;
|
||||
use WP_CLI\AiCommand\MCP\Servers\WP_CLI\Tools\CliCommands;
|
||||
use McpWp\AiCommand_Dependencies\McpWp\MCP\Server;
|
||||
use McpWp\AiCommand\MCP\Servers\WP_CLI\Tools\CliCommands;
|
||||
|
||||
class WP_CLI extends Server {
|
||||
public function __construct() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
<?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_Command;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\Utils;
|
||||
namespace McpWp\AiCommand\Utils;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use InvalidArgumentException;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\Utils;
|
||||
namespace McpWp\AiCommand\Utils;
|
||||
|
||||
use WP_CLI\Utils;
|
||||
use WP_CLI_Command;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\Tests\MCP\Client;
|
||||
namespace McpWp\AiCommand\Tests\MCP\Client;
|
||||
|
||||
use WP_CLI\Tests\TestCase;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue