mirror of
https://hk.gh-proxy.com/https://github.com/mcp-wp/ai-command.git
synced 2025-10-03 10:10:57 +08:00
Initial commit
This commit is contained in:
commit
374e6ce910
15 changed files with 855 additions and 0 deletions
18
.distignore
Normal file
18
.distignore
Normal file
|
@ -0,0 +1,18 @@
|
|||
.DS_Store
|
||||
.git
|
||||
.gitignore
|
||||
.gitlab-ci.yml
|
||||
.editorconfig
|
||||
.travis.yml
|
||||
behat.yml
|
||||
circle.yml
|
||||
phpcs.xml.dist
|
||||
phpunit.xml.dist
|
||||
bin/
|
||||
features/
|
||||
utils/
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.swp
|
||||
*.txt
|
||||
*.log
|
26
.editorconfig
Normal file
26
.editorconfig
Normal file
|
@ -0,0 +1,26 @@
|
|||
# This file is for unifying the coding style for different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
# WordPress Coding Standards
|
||||
# https://make.wordpress.org/core/handbook/coding-standards/
|
||||
|
||||
# From https://github.com/WordPress/wordpress-develop/blob/trunk/.editorconfig with a couple of additions.
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
|
||||
[{*.yml,*.feature,.jshintrc,*.json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{*.txt,wp-config-sample.php}]
|
||||
end_of_line = crlf
|
11
.github/ISSUE_TEMPLATE
vendored
Normal file
11
.github/ISSUE_TEMPLATE
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!--
|
||||
|
||||
Thanks for creating a new issue!
|
||||
|
||||
Found a bug or want to suggest an enhancement? Before completing your issue, please review our best practices: https://make.wordpress.org/cli/handbook/bug-reports/
|
||||
|
||||
Need help with something? GitHub issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support
|
||||
|
||||
You can safely delete this comment.
|
||||
|
||||
-->
|
16
.github/PULL_REQUEST_TEMPLATE
vendored
Normal file
16
.github/PULL_REQUEST_TEMPLATE
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!--
|
||||
|
||||
Thanks for submitting a pull request!
|
||||
|
||||
Please review our contributing guidelines if you haven't recently: https://make.wordpress.org/cli/handbook/contributing/#creating-a-pull-request
|
||||
|
||||
Here's an overview to our process:
|
||||
|
||||
1. One of the project committers will soon provide a code review: https://make.wordpress.org/cli/handbook/code-review/
|
||||
2. You are expected to address the code review comments in a timely manner (if we don't hear from you in two weeks, we'll consider your pull request abandoned).
|
||||
3. Please make sure to include functional tests for your changes.
|
||||
4. The reviewing committer will merge your pull request as soon as it passes code review (and provided it fits within the scope of the project).
|
||||
|
||||
You can safely delete this comment.
|
||||
|
||||
-->
|
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
wp-cli.local.yml
|
||||
node_modules/
|
||||
vendor/
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.swp
|
||||
*.txt
|
||||
*.log
|
||||
composer.lock
|
||||
phpunit.xml
|
||||
phpcs.xml
|
||||
.phpcs.xml
|
83
.travis.yml
Normal file
83
.travis.yml
Normal file
|
@ -0,0 +1,83 @@
|
|||
os: linux
|
||||
dist: xenial
|
||||
|
||||
language: php
|
||||
php: 7.4
|
||||
|
||||
services:
|
||||
- mysql
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
on_failure: change
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
env:
|
||||
global:
|
||||
- PATH="$TRAVIS_BUILD_DIR/vendor/bin:$PATH"
|
||||
- WP_CLI_BIN_DIR="$TRAVIS_BUILD_DIR/vendor/bin"
|
||||
|
||||
before_install:
|
||||
- |
|
||||
# Remove Xdebug for a huge performance increase:
|
||||
if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then
|
||||
phpenv config-rm xdebug.ini
|
||||
else
|
||||
echo "xdebug.ini does not exist"
|
||||
fi
|
||||
- |
|
||||
# Raise PHP memory limit to 2048MB
|
||||
echo 'memory_limit = 2048M' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
|
||||
- composer validate
|
||||
|
||||
install:
|
||||
- composer install
|
||||
- composer prepare-tests
|
||||
|
||||
script:
|
||||
- composer phpunit
|
||||
- composer behat || composer behat-rerun
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: test
|
||||
php: nightly
|
||||
env: WP_VERSION=trunk
|
||||
- stage: test
|
||||
php: 7.4
|
||||
env: WP_VERSION=latest
|
||||
- stage: test
|
||||
php: 7.3
|
||||
env: WP_VERSION=latest
|
||||
- stage: test
|
||||
php: 7.2
|
||||
env: WP_VERSION=latest
|
||||
- stage: test
|
||||
php: 7.1
|
||||
env: WP_VERSION=latest
|
||||
- stage: test
|
||||
php: 7.0
|
||||
env: WP_VERSION=latest
|
||||
- stage: test
|
||||
php: 5.6
|
||||
env: WP_VERSION=latest
|
||||
- stage: test
|
||||
php: 5.6
|
||||
env: WP_VERSION=3.7.11
|
||||
dist: trusty
|
||||
- stage: test
|
||||
php: 5.6
|
||||
env: WP_VERSION=trunk
|
||||
|
||||
allow_failures:
|
||||
- stage: test
|
||||
php: nightly
|
||||
env: WP_VERSION=trunk
|
8
CONTRIBUTING.md
Normal file
8
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
Contributing
|
||||
============
|
||||
|
||||
We appreciate you taking the initiative to contribute to this project.
|
||||
|
||||
Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation.
|
||||
|
||||
For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines.
|
59
README.md
Normal file
59
README.md
Normal file
|
@ -0,0 +1,59 @@
|
|||
swissspidy/ai-command
|
||||
=====================
|
||||
|
||||
|
||||
|
||||
[](https://travis-ci.org/swissspidy/ai-command)
|
||||
|
||||
Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support)
|
||||
|
||||
## Using
|
||||
|
||||
~~~
|
||||
wp hello-world
|
||||
~~~
|
||||
|
||||
## Installing
|
||||
|
||||
Installing this package requires WP-CLI v2.5 or greater. Update to the latest stable release with `wp cli update`.
|
||||
|
||||
Once you've done so, you can install the latest stable version of this package with:
|
||||
|
||||
```bash
|
||||
wp package install swissspidy/ai-command:@stable
|
||||
```
|
||||
|
||||
To install the latest development version of this package, use the following command instead:
|
||||
|
||||
```bash
|
||||
wp package install swissspidy/ai-command:dev-master
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We appreciate you taking the initiative to contribute to this project.
|
||||
|
||||
Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation.
|
||||
|
||||
For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines.
|
||||
|
||||
### Reporting a bug
|
||||
|
||||
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.
|
||||
|
||||
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/).
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
## Support
|
||||
|
||||
GitHub issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support
|
||||
|
||||
|
||||
*This README.md is generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). To suggest changes, please submit a pull request against the corresponding part of the codebase.*
|
17
ai-command.php
Normal file
17
ai-command.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand;
|
||||
|
||||
use WP_CLI;
|
||||
|
||||
if ( ! class_exists( '\WP_CLI' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ai_command_autoloader = __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
if ( file_exists( $ai_command_autoloader ) ) {
|
||||
require_once $ai_command_autoloader;
|
||||
}
|
||||
|
||||
WP_CLI::add_command( 'ai', AiCommand::class );
|
63
composer.json
Normal file
63
composer.json
Normal file
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"name": "swissspidy/ai-command",
|
||||
"type": "wp-cli-package",
|
||||
"description": "",
|
||||
"homepage": "https://github.com/swissspidy/ai-command",
|
||||
"license": "MIT",
|
||||
"authors": [],
|
||||
"require": {
|
||||
"gemini-api-php/client": "^1.7",
|
||||
"logiscape/mcp-sdk-php": "^1.0",
|
||||
"symfony/http-client": "^7.2",
|
||||
"wp-cli/wp-cli": "^2.12"
|
||||
},
|
||||
"require-dev": {
|
||||
"wp-cli/wp-cli-tests": "^v4.3.6"
|
||||
},
|
||||
"config": {
|
||||
"process-timeout": 7200,
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.x-dev"
|
||||
},
|
||||
"bundled": false,
|
||||
"commands": [
|
||||
"ai",
|
||||
"ai prompt"
|
||||
]
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WP_CLI\\AiCommand\\": "src/",
|
||||
"WP_CLI\\AiCommand\\MCP\\": "src/MCP"
|
||||
},
|
||||
"files": [
|
||||
"ai-command.php"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"scripts": {
|
||||
"behat": "run-behat-tests",
|
||||
"behat-rerun": "rerun-behat-tests",
|
||||
"lint": "run-linter-tests",
|
||||
"phpcs": "run-phpcs-tests",
|
||||
"phpunit": "run-php-unit-tests",
|
||||
"prepare-tests": "install-package-tests",
|
||||
"test": [
|
||||
"@lint",
|
||||
"@phpcs",
|
||||
"@phpunit",
|
||||
"@behat"
|
||||
]
|
||||
},
|
||||
"support": {
|
||||
"issues": "https://github.com/swissspidy/ai-command/issues"
|
||||
}
|
||||
}
|
10
features/load-wp-cli.feature
Normal file
10
features/load-wp-cli.feature
Normal file
|
@ -0,0 +1,10 @@
|
|||
Feature: Test that WP-CLI loads.
|
||||
|
||||
Scenario: WP-CLI loads for your tests
|
||||
Given a WP install
|
||||
|
||||
When I run `wp eval 'echo "Hello world.";'`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Hello world.
|
||||
"""
|
112
src/AiCommand.php
Normal file
112
src/AiCommand.php
Normal file
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand;
|
||||
|
||||
use Exception;
|
||||
use WP_CLI;
|
||||
use WP_CLI_Command;
|
||||
|
||||
/**
|
||||
*
|
||||
* Resources: File-like data that can be read by clients (like API responses or file contents)
|
||||
* Tools: Functions that can be called by the LLM (with user approval)
|
||||
* Prompts: Pre-written templates that help users accomplish specific tasks
|
||||
*
|
||||
* MCP follows a client-server architecture where:
|
||||
*
|
||||
* Hosts are LLM applications (like Claude Desktop or IDEs) that initiate connections
|
||||
* Clients maintain 1:1 connections with servers, inside the host application
|
||||
* Servers provide context, tools, and prompts to clients
|
||||
*/
|
||||
class AiCommand extends WP_CLI_Command {
|
||||
/**
|
||||
* Greets the world.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <prompt>
|
||||
* : AI prompt.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Greet the world.
|
||||
* $ wp ai "What are the titles of my last three posts?"
|
||||
* Success: Hello World!
|
||||
*
|
||||
* # Greet the world.
|
||||
* $ wp ai "create 10 test posts about swiss recipes and include generated featured images"
|
||||
* Success: Hello World!
|
||||
*
|
||||
* @when before_wp_load
|
||||
*
|
||||
* @param array $args Indexed array of positional arguments.
|
||||
* @param array $assoc_args Associative array of associative arguments.
|
||||
*/
|
||||
public function __invoke( $args, $assoc_args ) {
|
||||
$server = new MCP\Server();
|
||||
|
||||
$server->registerTool( [
|
||||
'name' => 'calculate_total',
|
||||
'description' => 'Calculates the total price.',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'price' => [ 'type' => 'integer', 'description' => 'The price of the item.' ],
|
||||
'quantity' => [ 'type' => 'integer', 'description' => 'The quantity of items.' ],
|
||||
],
|
||||
'required' => [ 'price', 'quantity' ],
|
||||
],
|
||||
'callable' => function ( $params ) {
|
||||
$price = $params['price'] ?? 0;
|
||||
$quantity = $params['quantity'] ?? 1;
|
||||
|
||||
return $price * $quantity;
|
||||
},
|
||||
] );
|
||||
|
||||
$server->registerTool( [
|
||||
'name' => 'greet',
|
||||
'description' => 'Greets the user.',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'name' => [ 'type' => 'string', 'description' => 'The name of the user.' ],
|
||||
],
|
||||
'required' => [ 'name' ],
|
||||
],
|
||||
'callable' => function ( $params ) {
|
||||
return "Hello, " . $params['name'] . "!";
|
||||
},
|
||||
] );
|
||||
|
||||
// Register resources:
|
||||
$server->registerResource( [
|
||||
'name' => 'users',
|
||||
'uri' => 'data://users',
|
||||
'description' => 'List of users',
|
||||
'mimeType' => 'application/json',
|
||||
'dataKey' => 'users', // This tells getResourceData() to look in the $data array
|
||||
] );
|
||||
|
||||
$server->registerResource( [
|
||||
'name' => 'product_catalog',
|
||||
'uri' => 'file://./products.json',
|
||||
'description' => 'Product catalog',
|
||||
'mimeType' => 'application/json',
|
||||
'filePath' => './products.json', // This tells getResourceData() to read from a file
|
||||
] );
|
||||
|
||||
$client = new MCP\Client( $server );
|
||||
|
||||
$result = $client->callGemini( [
|
||||
[
|
||||
"role" => "user",
|
||||
"parts" => [
|
||||
"text" => $args[0]
|
||||
]
|
||||
]
|
||||
] );
|
||||
|
||||
WP_CLI::success( $result );
|
||||
}
|
||||
}
|
140
src/MCP/Client.php
Normal file
140
src/MCP/Client.php
Normal file
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\MCP;
|
||||
|
||||
use Exception;
|
||||
|
||||
class Client {
|
||||
|
||||
private $server; // Instance of MCPServer
|
||||
|
||||
public function __construct( Server $server ) {
|
||||
$this->server = $server;
|
||||
}
|
||||
|
||||
public function sendRequest( $method, $params = [] ) {
|
||||
$request = [
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => $method,
|
||||
'params' => $params,
|
||||
'id' => uniqid(), // Generate a unique ID for each request
|
||||
];
|
||||
|
||||
$requestData = json_encode( $request );
|
||||
$responseData = $this->server->processRequest( $requestData );
|
||||
$response = json_decode( $responseData, true );
|
||||
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
throw new Exception( 'Invalid JSON response: ' . json_last_error_msg() );
|
||||
}
|
||||
|
||||
if ( isset( $response['error'] ) ) {
|
||||
throw new Exception( "JSON-RPC Error: " . $response['error']['message'], $response['error']['code'] );
|
||||
}
|
||||
|
||||
return $response['result'];
|
||||
}
|
||||
|
||||
public function __call( $name, $arguments ) { // Magic method for calling any method
|
||||
return $this->sendRequest( $name, $arguments[0] ?? [] );
|
||||
}
|
||||
|
||||
public function list_resources() {
|
||||
return $this->sendRequest( 'resources/list' );
|
||||
}
|
||||
|
||||
public function read_resource( $uri ) {
|
||||
return $this->sendRequest( 'resources/read', [ 'uri' => $uri ] );
|
||||
}
|
||||
|
||||
public function callGemini( $contents ) {
|
||||
$capabilities = $this->get_capabilities();
|
||||
|
||||
$tools = [];
|
||||
|
||||
foreach ( $capabilities['methods'] ?? [] as $tool ) {
|
||||
$tools[] = [
|
||||
"name" => $tool['name'],
|
||||
"description" => $tool['description'] ?? "", // Provide a description
|
||||
"parameters" => $tool['inputSchema'] ?? [], // Provide the inputSchema
|
||||
];
|
||||
}
|
||||
|
||||
\WP_CLI::log( 'Calling Gemini...' . json_encode( [
|
||||
|
||||
'contents' => $contents,
|
||||
'tools' => [
|
||||
'function_declarations' => $tools,
|
||||
],
|
||||
] ) );
|
||||
|
||||
$GOOGLE_API_KEY = getenv( 'GEMINI_API_KEY' );
|
||||
|
||||
$response = \WP_CLI\Utils\http_request(
|
||||
'POST',
|
||||
// "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=$GOOGLE_API_KEY",
|
||||
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=$GOOGLE_API_KEY",
|
||||
json_encode( [
|
||||
'contents' => $contents,
|
||||
'tools' => [
|
||||
'function_declarations' => $tools,
|
||||
],
|
||||
]
|
||||
),
|
||||
[
|
||||
'Content-Type' => 'application/json'
|
||||
]
|
||||
);
|
||||
|
||||
$data = json_decode( $response->body );
|
||||
|
||||
\WP_CLI::log( 'Receiving response...' . json_encode( $data ) );
|
||||
|
||||
$new_contents = $contents;
|
||||
|
||||
foreach ( $data->candidates[0]->content->parts as $part ) {
|
||||
// Check for tool calls in Gemini response
|
||||
if ( isset( $part->functionCall ) ) {
|
||||
$name = $part->functionCall->name;
|
||||
$args = (array) $part->functionCall->args;
|
||||
|
||||
$functionResult = $this->$name( $args );
|
||||
|
||||
\WP_CLI::log( "Calling function $name... Result:" . print_r( $functionResult, true ) );
|
||||
|
||||
$new_contents[] = [
|
||||
'role' => 'model',
|
||||
'parts' => [
|
||||
$part
|
||||
]
|
||||
];
|
||||
$new_contents[] = [
|
||||
'role' => 'user',
|
||||
'parts' => [
|
||||
[
|
||||
'functionResponse' => [
|
||||
'name' => $name,
|
||||
'response' => [
|
||||
'name' => $name,
|
||||
'content' => $functionResult,
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ( $new_contents !== $contents ) {
|
||||
return $this->callGemini( $new_contents );
|
||||
}
|
||||
|
||||
foreach ( $data->candidates[0]->content->parts as $part ) {
|
||||
if ( isset( $part->text ) ) {
|
||||
return $part->text;
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown!';
|
||||
}
|
||||
}
|
277
src/MCP/Server.php
Normal file
277
src/MCP/Server.php
Normal file
|
@ -0,0 +1,277 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\MCP;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class Server {
|
||||
|
||||
private array $data = [];
|
||||
private array $tools = [];
|
||||
private array $resources = [];
|
||||
|
||||
public function __construct() {
|
||||
// Sample data (replace with your actual data handling)
|
||||
$this->data['users'] = [
|
||||
[ 'id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com' ],
|
||||
[ 'id' => 2, 'name' => 'Bob', 'email' => 'bob@example.com' ],
|
||||
];
|
||||
$this->data['products'] = [
|
||||
[ 'id' => 101, 'name' => 'Product A', 'price' => 20 ],
|
||||
[ 'id' => 102, 'name' => 'Product B', 'price' => 30 ],
|
||||
];
|
||||
}
|
||||
|
||||
public function registerTool( $toolDefinition ): void {
|
||||
if ( ! is_array( $toolDefinition ) || ! isset( $toolDefinition['name'] ) || ! is_callable( $toolDefinition['callable'] ) ) {
|
||||
throw new InvalidArgumentException( "Invalid tool definition. Must be an array with 'name' and 'callable'." );
|
||||
}
|
||||
|
||||
$name = $toolDefinition['name'];
|
||||
$callable = $toolDefinition['callable'];
|
||||
$description = $toolDefinition['description'] ?? null;
|
||||
$inputSchema = $toolDefinition['inputSchema'] ?? null;
|
||||
|
||||
$this->tools[ $name ] = [
|
||||
'name' => $name,
|
||||
'callable' => $callable,
|
||||
'description' => $description,
|
||||
'inputSchema' => $inputSchema,
|
||||
];
|
||||
}
|
||||
|
||||
public function registerResource( $resourceDefinition ) {
|
||||
// Validate the resource definition (similar to tool validation)
|
||||
if ( ! is_array( $resourceDefinition ) || ! isset( $resourceDefinition['name'] ) || ! isset( $resourceDefinition['uri'] ) ) {
|
||||
throw new InvalidArgumentException( "Invalid resource definition." );
|
||||
}
|
||||
|
||||
$this->resources[ $resourceDefinition['name'] ] = $resourceDefinition;
|
||||
}
|
||||
|
||||
public function getCapabilities(): array {
|
||||
$capabilities = [
|
||||
'version' => '1.0', // MCP version (adjust as needed)
|
||||
'methods' => [],
|
||||
'data_resources' => [],
|
||||
];
|
||||
|
||||
foreach ( $this->tools as $tool ) { // Iterate through the tools array
|
||||
$capabilities['methods'][] = [ // Add each tool as an element in the array
|
||||
'name' => $tool['name'],
|
||||
'description' => $tool['description'],
|
||||
'inputSchema' => $tool['inputSchema'],
|
||||
];
|
||||
}
|
||||
|
||||
// Add data resources
|
||||
// Add resources to capabilities
|
||||
foreach ( $this->resources as $resource ) {
|
||||
$capabilities['data_resources'] = [
|
||||
'name' => $resource['name'],
|
||||
// You can add more details about the resource here if needed
|
||||
];
|
||||
}
|
||||
|
||||
return $capabilities;
|
||||
}
|
||||
|
||||
public function handleRequest( $requestData ): false|string {
|
||||
$request = json_decode( $requestData, true );
|
||||
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
return $this->createErrorResponse( null, 'Invalid JSON', - 32700 ); // Parse error
|
||||
}
|
||||
|
||||
if ( ! isset( $request['jsonrpc'] ) || $request['jsonrpc'] !== '2.0' ) {
|
||||
return $this->createErrorResponse( $request['id'] ?? null, 'Invalid JSON-RPC version', - 32600 ); // Invalid Request
|
||||
}
|
||||
|
||||
if ( ! isset( $request['method'] ) ) {
|
||||
return $this->createErrorResponse( $request['id'] ?? null, 'Missing method', - 32600 ); // Invalid Request
|
||||
}
|
||||
|
||||
$method = $request['method'];
|
||||
$params = $request['params'] ?? [];
|
||||
$id = $request['id'] ?? null;
|
||||
|
||||
if ( $method === 'get_capabilities' ) { // Handle capabilities request
|
||||
$capabilities = $this->getCapabilities();
|
||||
|
||||
return $this->createSuccessResponse( $id, $capabilities );
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if it's a data access request (starts with "get_")
|
||||
if ( str_starts_with( $method, 'get_' ) ) {
|
||||
$resource = substr( $method, 4 ); // Extract the resource name (e.g., "users" from "get_users")
|
||||
|
||||
if ( isset( $this->data[ $resource ] ) ) {
|
||||
$result = $this->handleGetRequest( '/' . $resource, $params ); // Re-use handleGetRequest
|
||||
} else if ( isset( $this->data["{$resource}s"] ) ) {
|
||||
$result = $this->handleGetRequest( '/' . "{$resource}s", $params ); // Re-use handleGetRequest
|
||||
} else {
|
||||
return $this->createErrorResponse( $id, 'Resource not found', - 32601 ); // Method not found
|
||||
}
|
||||
|
||||
} else if ( $method === 'resources/list' ) {
|
||||
$result = $this->listResources();
|
||||
} elseif ( $method === 'resources/read' ) {
|
||||
$result = $this->readResource( $params['uri'] ?? null );
|
||||
} else { // Treat as a tool call
|
||||
|
||||
$tool = $this->tools[ $method ] ?? null;
|
||||
if ( ! $tool ) {
|
||||
return $this->createErrorResponse( $id, 'Method not found', - 32601 );
|
||||
}
|
||||
|
||||
// Validate input parameters against the schema
|
||||
$inputSchema = $tool['inputSchema'] ?? null;
|
||||
if ( $inputSchema ) {
|
||||
$isValid = $this->validateInput( $params, $inputSchema );
|
||||
if ( ! $isValid['valid'] ) {
|
||||
return $this->createErrorResponse( $id, 'Invalid input parameters: ' . implode( ", ", $isValid['errors'] ), - 32602 ); // Invalid params
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$result = call_user_func( $tool['callable'], $params ); // Call the 'callable' property
|
||||
|
||||
return $this->createSuccessResponse( $id, $result ); // Return success immediately
|
||||
|
||||
}
|
||||
|
||||
return $this->createSuccessResponse( $id, $result );
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
return $this->createErrorResponse( $id, $e->getMessage(), - 32000 ); // Application error
|
||||
}
|
||||
}
|
||||
|
||||
private function listResources() {
|
||||
$result = [];
|
||||
foreach ( $this->resources as $resource ) {
|
||||
$result[] = [
|
||||
'uri' => $resource['uri'],
|
||||
'name' => $resource['name'],
|
||||
'description' => $resource['description'] ?? null,
|
||||
'mimeType' => $resource['mimeType'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function readResource( $uri ) {
|
||||
// Find the resource by URI
|
||||
$resource = null;
|
||||
foreach ( $this->resources as $r ) {
|
||||
if ( $r['uri'] === $uri ) {
|
||||
$resource = $r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $resource ) {
|
||||
throw new Exception( "Resource not found." );
|
||||
}
|
||||
|
||||
// Access the resource data (replace with your actual data access logic)
|
||||
$data = $this->getResourceData( $resource );
|
||||
|
||||
// Determine if it's text or binary
|
||||
$isBinary = isset( $resource['mimeType'] ) && ! str_starts_with( $resource['mimeType'], 'text/' );
|
||||
|
||||
return [
|
||||
'uri' => $resource['uri'],
|
||||
'mimeType' => $resource['mimeType'] ?? null,
|
||||
( $isBinary ? 'blob' : 'text' ) => $data,
|
||||
];
|
||||
}
|
||||
|
||||
private function getResourceData( $resource ) {
|
||||
// Replace this with your actual logic to access the resource data
|
||||
// based on the resource definition.
|
||||
|
||||
// Example: If the resource is a file, read the file contents.
|
||||
if ( isset( $resource['filePath'] ) ) {
|
||||
return file_get_contents( $resource['filePath'] );
|
||||
}
|
||||
|
||||
// Example: If the resource is in the $data array, return the data.
|
||||
if ( isset( $resource['dataKey'] ) ) {
|
||||
return $this->data[ $resource['dataKey'] ];
|
||||
}
|
||||
|
||||
//... other data access logic...
|
||||
|
||||
throw new Exception( "Unable to access resource data." );
|
||||
}
|
||||
|
||||
private function validateInput( $input, $schema ): array {
|
||||
// Basic input validation (you might want to use a dedicated JSON schema validator library)
|
||||
$errors = [];
|
||||
foreach ( $schema['properties'] ?? [] as $paramName => $paramSchema ) {
|
||||
if ( isset( $paramSchema['required'] ) && $paramSchema['required'] === true && ! isset( $input[ $paramName ] ) ) {
|
||||
$errors[] = $paramName . " is required";
|
||||
}
|
||||
// Add more validation rules as needed (e.g., type checking)
|
||||
if ( isset( $input[ $paramName ] ) && isset( $paramSchema['type'] ) ) {
|
||||
$inputType = gettype( $input[ $paramName ] );
|
||||
if ( $inputType !== $paramSchema['type'] ) {
|
||||
$errors[] = $paramName . " must be of type " . $paramSchema['type'] . " but " . $inputType . " was given.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [ 'valid' => empty( $errors ), 'errors' => $errors ];
|
||||
}
|
||||
|
||||
private function handleGetRequest( $path, $params ) {
|
||||
$parts = explode( '/', ltrim( $path, '/' ) );
|
||||
$resource = $parts[0];
|
||||
$id = $params['id'] ?? null; // Simplified parameter handling
|
||||
|
||||
if ( isset( $this->data[ $resource ] ) ) {
|
||||
$data = $this->data[ $resource ];
|
||||
|
||||
if ( $id !== null ) {
|
||||
foreach ( $data as $item ) {
|
||||
if ( $item['id'] == $id ) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
throw new Exception( 'Resource not found' );
|
||||
} else {
|
||||
return $data;
|
||||
}
|
||||
} else {
|
||||
throw new Exception( 'Resource not found' );
|
||||
}
|
||||
}
|
||||
|
||||
private function createSuccessResponse( $id, $result ): false|string {
|
||||
return json_encode( [
|
||||
'jsonrpc' => '2.0',
|
||||
'result' => $result,
|
||||
'id' => $id,
|
||||
] );
|
||||
}
|
||||
|
||||
private function createErrorResponse( $id, $message, $code ): false|string {
|
||||
return json_encode( [
|
||||
'jsonrpc' => '2.0',
|
||||
'error' => [
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
],
|
||||
'id' => $id,
|
||||
] );
|
||||
}
|
||||
|
||||
public function processRequest( $requestData ): false|string {
|
||||
return $this->handleRequest( $requestData );
|
||||
}
|
||||
}
|
||||
|
2
wp-cli.yml
Normal file
2
wp-cli.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
require:
|
||||
- ai-command.php
|
Loading…
Add table
Add a link
Reference in a new issue