mirror of
https://hk.gh-proxy.com/https://github.com/mcp-wp/ai-command.git
synced 2025-10-03 10:10:57 +08:00
testing setup, lint fixes
This commit is contained in:
parent
c75f95acd5
commit
bf108dc87d
11 changed files with 424 additions and 267 deletions
97
.github/workflows/testing.yml
vendored
Normal file
97
.github/workflows/testing.yml
vendored
Normal file
|
@ -0,0 +1,97 @@
|
|||
name: Testing
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
behat:
|
||||
name: Functional / PHP ${{ matrix.php }}
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.2']
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: wp_cli_test
|
||||
MYSQL_USER: wp_cli_test
|
||||
MYSQL_PASSWORD: password1
|
||||
MYSQL_HOST: 127.0.0.1
|
||||
ports:
|
||||
- 3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
tools: composer
|
||||
|
||||
- name: Install composer packages
|
||||
run: composer install
|
||||
|
||||
- name: Run Behat
|
||||
run: composer behat
|
||||
env:
|
||||
WP_CLI_TEST_DBUSER: wp_cli_test
|
||||
WP_CLI_TEST_DBPASS: password1
|
||||
WP_CLI_TEST_DBNAME: wp_cli_test
|
||||
WP_CLI_TEST_DBHOST: 127.0.0.1:${{ job.services.mysql.ports[3306] }}
|
||||
|
||||
unit: #-----------------------------------------------------------------------
|
||||
name: Unit test / PHP ${{ matrix.php }}
|
||||
strategy:
|
||||
matrix:
|
||||
php: [ '8.2' ]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up PHP environment
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '${{ matrix.php }}'
|
||||
ini-values: zend.assertions=1, error_reporting=-1, display_errors=On
|
||||
coverage: none
|
||||
tools: composer,cs2pr
|
||||
|
||||
- name: Install Composer dependencies & cache dependencies
|
||||
uses: ramsey/composer-install@v3
|
||||
env:
|
||||
COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }}
|
||||
with:
|
||||
# Bust the cache at least once a month - output format: YYYY-MM.
|
||||
custom-cache-suffix: $(date -u "+%Y-%m")
|
||||
|
||||
- name: Grab PHPUnit version
|
||||
id: phpunit_version
|
||||
run: echo "VERSION=$(vendor/bin/phpunit --version | grep --only-matching --max-count=1 --extended-regexp '\b[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
|
||||
|
||||
# PHPUnit 10 may fail a test run when the "old" configuration format is used.
|
||||
# Luckily, there is a build-in migration tool since PHPUnit 9.3.
|
||||
- name: Migrate PHPUnit configuration for PHPUnit 10+
|
||||
if: ${{ startsWith( steps.phpunit_version.outputs.VERSION, '10.' ) }}
|
||||
continue-on-error: true
|
||||
run: composer phpunit -- --migrate-configuration
|
||||
|
||||
- name: Setup problem matcher to provide annotations for PHPUnit
|
||||
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||
|
||||
- name: Run PHPUnit
|
||||
run: composer phpunit
|
1
.phpunit.result.cache
Normal file
1
.phpunit.result.cache
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":1,"defects":[],"times":{"AiCommand\\Tests\\MCP\\Client\\ClientTest::test_runs":0.001}}
|
83
.travis.yml
83
.travis.yml
|
@ -1,83 +0,0 @@
|
|||
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
|
7
behat.yml
Normal file
7
behat.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
default:
|
||||
suites:
|
||||
default:
|
||||
contexts:
|
||||
- WP_CLI\Tests\Context\FeatureContext
|
||||
paths:
|
||||
- features
|
|
@ -6,13 +6,12 @@
|
|||
"license": "MIT",
|
||||
"authors": [],
|
||||
"require": {
|
||||
"gemini-api-php/client": "^1.7",
|
||||
"logiscape/mcp-sdk-php": "^1.0",
|
||||
"symfony/http-client": "^7.2",
|
||||
"php": "^8.2",
|
||||
"wp-cli/wp-cli": "^2.12"
|
||||
},
|
||||
"require-dev": {
|
||||
"wp-cli/wp-cli-tests": "^v4.3.6"
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"wp-cli/wp-cli-tests": "^v4.3.9"
|
||||
},
|
||||
"config": {
|
||||
"process-timeout": 7200,
|
||||
|
@ -48,6 +47,7 @@
|
|||
"behat-rerun": "rerun-behat-tests",
|
||||
"lint": "run-linter-tests",
|
||||
"phpcs": "run-phpcs-tests",
|
||||
"phpcbf": "run-phpcbf-cleanup",
|
||||
"phpunit": "run-php-unit-tests",
|
||||
"prepare-tests": "install-package-tests",
|
||||
"test": [
|
||||
|
|
54
phpcs.xml.dist
Normal file
54
phpcs.xml.dist
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0"?>
|
||||
<ruleset name="ai-command">
|
||||
<description>Custom ruleset for ai-command</description>
|
||||
|
||||
<!--
|
||||
#############################################################################
|
||||
COMMAND LINE ARGUMENTS
|
||||
For help understanding this file: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml
|
||||
For help using PHPCS: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage
|
||||
#############################################################################
|
||||
-->
|
||||
|
||||
<!-- What to scan. -->
|
||||
<file>.</file>
|
||||
|
||||
<!-- Show progress. -->
|
||||
<arg value="p"/>
|
||||
|
||||
<!-- Strip the filepaths down to the relevant bit. -->
|
||||
<arg name="basepath" value="./"/>
|
||||
|
||||
<!-- Check up to 8 files simultaneously. -->
|
||||
<arg name="parallel" value="8"/>
|
||||
|
||||
<!--
|
||||
#############################################################################
|
||||
USE THE WP_CLI_CS RULESET
|
||||
#############################################################################
|
||||
-->
|
||||
|
||||
<rule ref="WP_CLI_CS"/>-
|
||||
|
||||
<!--
|
||||
#############################################################################
|
||||
PROJECT SPECIFIC CONFIGURATION FOR SNIFFS
|
||||
#############################################################################
|
||||
-->
|
||||
|
||||
<!-- For help understanding the `testVersion` configuration setting:
|
||||
https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions -->
|
||||
<config name="testVersion" value="8.2-"/>
|
||||
|
||||
<!-- Verify that everything in the global namespace is either namespaced or prefixed.
|
||||
See: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#naming-conventions-prefix-everything-in-the-global-namespace -->
|
||||
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
|
||||
<properties>
|
||||
<property name="prefixes" type="array">
|
||||
<element value="WP_CLI\AiCommand"/><!-- Namespaces. -->
|
||||
<element value="ai_command"/><!-- Global variables and such. -->
|
||||
</property>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
</ruleset>
|
26
phpunit.xml.dist
Normal file
26
phpunit.xml.dist
Normal file
|
@ -0,0 +1,26 @@
|
|||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/4.8/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
backupGlobals="false"
|
||||
beStrictAboutCoversAnnotation="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||
beStrictAboutTodoAnnotatedTests="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertDeprecationsToExceptions="true"
|
||||
colors="true"
|
||||
verbose="true">
|
||||
<testsuites>
|
||||
<testsuite name="ai-command tests">
|
||||
<directory suffix="Test.php">tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<coverage processUncoveredFiles="false">
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
</phpunit>
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace WP_CLI\AiCommand;
|
||||
|
||||
use Exception;
|
||||
use WP_CLI;
|
||||
use WP_CLI_Command;
|
||||
|
||||
|
@ -45,73 +44,95 @@ class AiCommand extends WP_CLI_Command {
|
|||
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.' ],
|
||||
$server->register_tool(
|
||||
[
|
||||
'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' ],
|
||||
],
|
||||
'required' => [ 'price', 'quantity' ],
|
||||
],
|
||||
'callable' => function ( $params ) {
|
||||
$price = $params['price'] ?? 0;
|
||||
$quantity = $params['quantity'] ?? 1;
|
||||
'callable' => function ( $params ) {
|
||||
$price = $params['price'] ?? 0;
|
||||
$quantity = $params['quantity'] ?? 1;
|
||||
|
||||
return $price * $quantity;
|
||||
},
|
||||
] );
|
||||
return $price * $quantity;
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
$server->registerTool( [
|
||||
'name' => 'greet',
|
||||
'description' => 'Greets the user.',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'name' => [ 'type' => 'string', 'description' => 'The name of the user.' ],
|
||||
$server->register_tool(
|
||||
[
|
||||
'name' => 'greet',
|
||||
'description' => 'Greets the user.',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'name' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The name of the user.',
|
||||
],
|
||||
],
|
||||
'required' => [ 'name' ],
|
||||
],
|
||||
'required' => [ 'name' ],
|
||||
],
|
||||
'callable' => function ( $params ) {
|
||||
return "Hello, " . $params['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->register_resource(
|
||||
[
|
||||
'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
|
||||
] );
|
||||
$server->register_resource(
|
||||
[
|
||||
'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 );
|
||||
|
||||
$server->registerTool( [
|
||||
'name' => 'generate_image',
|
||||
'description' => 'Generates an image.',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'prompt' => [ 'type' => 'string', 'description' => 'The prompt for generating the image.' ],
|
||||
$server->register_tool(
|
||||
[
|
||||
'name' => 'generate_image',
|
||||
'description' => 'Generates an image.',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'prompt' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The prompt for generating the image.',
|
||||
],
|
||||
],
|
||||
'required' => [ 'prompt' ],
|
||||
],
|
||||
'required' => [ 'prompt' ],
|
||||
],
|
||||
'callable' => function ( $params ) use ( $client ) {
|
||||
return $client->get_image_from_ai_service( $params['prompt'] );
|
||||
},
|
||||
] );
|
||||
'callable' => function ( $params ) use ( $client ) {
|
||||
return $client->get_image_from_ai_service( $params['prompt'] );
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
$result = $client->call_ai_service_with_prompt( $args[0] );
|
||||
|
||||
|
|
|
@ -23,39 +23,40 @@ class Client {
|
|||
$this->server = $server;
|
||||
}
|
||||
|
||||
public function sendRequest( $method, $params = [] ) {
|
||||
public function send_request( $method, $params = [] ) {
|
||||
$request = [
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => $method,
|
||||
'params' => $params,
|
||||
'id' => uniqid(), // Generate a unique ID for each request
|
||||
'id' => uniqid( '', true ), // Generate a unique ID for each request
|
||||
];
|
||||
|
||||
$requestData = json_encode( $request );
|
||||
$responseData = $this->server->processRequest( $requestData );
|
||||
$response = json_decode( $responseData, true );
|
||||
$request_data = json_encode( $request );
|
||||
$response_data = $this->server->process_request( $request_data );
|
||||
$response = json_decode( $response_data, 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'] );
|
||||
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 __call( $name, $arguments ) {
|
||||
// Magic method for calling any method
|
||||
return $this->send_request( $name, $arguments[0] ?? [] );
|
||||
}
|
||||
|
||||
public function list_resources() {
|
||||
return $this->sendRequest( 'resources/list' );
|
||||
return $this->send_request( 'resources/list' );
|
||||
}
|
||||
|
||||
public function read_resource( $uri ) {
|
||||
return $this->sendRequest( 'resources/read', [ 'uri' => $uri ] );
|
||||
return $this->send_request( 'resources/read', [ 'uri' => $uri ] );
|
||||
}
|
||||
|
||||
// Must not have the same name as the tool, otherwise it takes precedence.
|
||||
|
@ -69,11 +70,11 @@ class Client {
|
|||
);
|
||||
|
||||
try {
|
||||
$service = ai_services()->get_available_service(
|
||||
$service = ai_services()->get_available_service(
|
||||
[
|
||||
'capabilities' => [
|
||||
AI_Capability::IMAGE_GENERATION,
|
||||
]
|
||||
],
|
||||
]
|
||||
);
|
||||
$candidates = $service
|
||||
|
@ -94,17 +95,17 @@ class Client {
|
|||
$image_url = '';
|
||||
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_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() );
|
||||
$filename = tempnam( '/tmp', 'ai-generated-image' );
|
||||
$parts = explode( '/', $part->get_mime_type() );
|
||||
$extension = $parts[1];
|
||||
rename( $filename, $filename . "." . $extension );
|
||||
$filename .= "." . $extension;
|
||||
rename( $filename, $filename . '.' . $extension );
|
||||
$filename .= '.' . $extension;
|
||||
|
||||
file_put_contents($filename, $image_blob->get_binary_data() );
|
||||
file_put_contents( $filename, $image_blob->get_binary_data() );
|
||||
|
||||
$image_url = $filename;
|
||||
}
|
||||
|
@ -149,9 +150,9 @@ class Client {
|
|||
|
||||
foreach ( $capabilities['methods'] ?? [] as $tool ) {
|
||||
$function_declarations[] = [
|
||||
"name" => $tool['name'],
|
||||
"description" => $tool['description'] ?? "", // Provide a description
|
||||
"parameters" => $tool['inputSchema'] ?? [], // Provide the inputSchema
|
||||
'name' => $tool['name'],
|
||||
'description' => $tool['description'] ?? '', // Provide a description
|
||||
'parameters' => $tool['inputSchema'] ?? [], // Provide the inputSchema
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -167,11 +168,11 @@ class Client {
|
|||
AI_Capability::MULTIMODAL_INPUT,
|
||||
AI_Capability::TEXT_GENERATION,
|
||||
AI_Capability::FUNCTION_CALLING,
|
||||
]
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// \WP_CLI::log( "Making request..." . print_r( $contents, true ) );
|
||||
\WP_CLI::log( 'Making request...' . print_r( $contents, true ) );
|
||||
|
||||
$candidates = $service
|
||||
->get_model(
|
||||
|
@ -190,12 +191,12 @@ class Client {
|
|||
$text = '';
|
||||
foreach ( $candidates->get( 0 )->get_content()->get_parts() as $part ) {
|
||||
if ( $part instanceof Text_Part ) {
|
||||
if ( $text !== '' ) {
|
||||
if ( '' !== $text ) {
|
||||
$text .= "\n\n";
|
||||
}
|
||||
$text .= $part->get_text();
|
||||
} elseif ( $part instanceof Function_Call_Part ) {
|
||||
var_dump('call function', $part);
|
||||
var_dump( 'call function', $part );
|
||||
$function_result = $this->{$part->get_name()}( $part->get_args() );
|
||||
|
||||
// Odd limitation of add_function_response_part().
|
||||
|
@ -210,8 +211,8 @@ class Client {
|
|||
$new_contents[] = new Content( Content_Role::MODEL, $parts );
|
||||
|
||||
$parts = new Parts();
|
||||
$parts->add_function_response_part( $part->get_id(),$part->get_name(), $function_result );
|
||||
$content = new Content( Content_Role::USER, $parts );
|
||||
$parts->add_function_response_part( $part->get_id(), $part->get_name(), $function_result );
|
||||
$content = new Content( Content_Role::USER, $parts );
|
||||
$new_contents[] = $content;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,50 +7,66 @@ use InvalidArgumentException;
|
|||
|
||||
class Server {
|
||||
|
||||
private array $data = [];
|
||||
private array $tools = [];
|
||||
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' ],
|
||||
[
|
||||
'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 ],
|
||||
[
|
||||
'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'] ) ) {
|
||||
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 = $toolDefinition['name'];
|
||||
$callable = $toolDefinition['callable'];
|
||||
$description = $toolDefinition['description'] ?? null;
|
||||
$inputSchema = $toolDefinition['inputSchema'] ?? null;
|
||||
$name = $tool_definition['name'];
|
||||
$callable = $tool_definition['callable'];
|
||||
$description = $tool_definition['description'] ?? null;
|
||||
$input_schema = $tool_definition['inputSchema'] ?? null;
|
||||
|
||||
$this->tools[ $name ] = [
|
||||
'name' => $name,
|
||||
'callable' => $callable,
|
||||
'description' => $description,
|
||||
'inputSchema' => $inputSchema,
|
||||
'inputSchema' => $input_schema,
|
||||
];
|
||||
}
|
||||
|
||||
public function registerResource( $resourceDefinition ) {
|
||||
public function register_resource( array $resource_definition ) {
|
||||
// Validate the resource definition (similar to tool validation)
|
||||
if ( ! is_array( $resourceDefinition ) || ! isset( $resourceDefinition['name'] ) || ! isset( $resourceDefinition['uri'] ) ) {
|
||||
throw new InvalidArgumentException( "Invalid resource definition." );
|
||||
if ( ! isset( $resource_definition['name'] ) || ! isset( $resource_definition['uri'] ) ) {
|
||||
throw new InvalidArgumentException( 'Invalid resource definition.' );
|
||||
}
|
||||
|
||||
$this->resources[ $resourceDefinition['name'] ] = $resourceDefinition;
|
||||
$this->resources[ $resource_definition['name'] ] = $resource_definition;
|
||||
}
|
||||
|
||||
public function getCapabilities(): array {
|
||||
public function get_capabilities(): array {
|
||||
$capabilities = [
|
||||
'version' => '1.0', // MCP version (adjust as needed)
|
||||
'methods' => [],
|
||||
|
@ -77,29 +93,29 @@ class Server {
|
|||
return $capabilities;
|
||||
}
|
||||
|
||||
public function handleRequest( $requestData ): false|string {
|
||||
$request = json_decode( $requestData, true );
|
||||
public function handle_request( string $request_data ): false|string {
|
||||
$request = json_decode( $request_data, true );
|
||||
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
return $this->createErrorResponse( null, 'Invalid JSON', - 32700 ); // Parse error
|
||||
return $this->create_error_response( 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['jsonrpc'] ) || '2.0' !== $request['jsonrpc'] ) {
|
||||
return $this->create_error_response( $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
|
||||
return $this->create_error_response( $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();
|
||||
if ( 'get_capabilities' === $method ) { // Handle capabilities request
|
||||
$capabilities = $this->get_capabilities();
|
||||
|
||||
return $this->createSuccessResponse( $id, $capabilities );
|
||||
return $this->create_success_response( $id, $capabilities );
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -108,48 +124,46 @@ class Server {
|
|||
$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
|
||||
$result = $this->handle_get_request( '/' . $resource, $params ); // Re-use handleGetRequest
|
||||
} elseif ( isset( $this->data[ "{$resource}s" ] ) ) {
|
||||
$result = $this->handle_get_request( '/' . "{$resource}s", $params ); // Re-use handleGetRequest
|
||||
} else {
|
||||
return $this->createErrorResponse( $id, 'Resource not found', - 32601 ); // Method not found
|
||||
return $this->create_error_response( $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 );
|
||||
} elseif ( 'resources/list' === $method ) {
|
||||
$result = $this->list_resources();
|
||||
} elseif ( 'resources/read' === $method ) {
|
||||
$result = $this->read_resource( $params['uri'] ?? null );
|
||||
} else { // Treat as a tool call
|
||||
|
||||
$tool = $this->tools[ $method ] ?? null;
|
||||
if ( ! $tool ) {
|
||||
return $this->createErrorResponse( $id, 'Method not found', - 32601 );
|
||||
return $this->create_error_response( $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
|
||||
$input_schema = $tool['inputSchema'] ?? null;
|
||||
if ( $input_schema ) {
|
||||
$is_valid = $this->validate_input( $params, $input_schema );
|
||||
if ( ! $is_valid['valid'] ) {
|
||||
return $this->create_error_response( $id, 'Invalid input parameters: ' . implode( ', ', $is_valid['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->create_success_response( $id, $result ); // Return success immediately
|
||||
|
||||
}
|
||||
|
||||
return $this->createSuccessResponse( $id, $result );
|
||||
return $this->create_success_response( $id, $result );
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
return $this->createErrorResponse( $id, $e->getMessage(), - 32000 ); // Application error
|
||||
return $this->create_error_response( $id, $e->getMessage(), - 32000 ); // Application error
|
||||
}
|
||||
}
|
||||
|
||||
private function listResources() {
|
||||
private function list_resources() {
|
||||
$result = [];
|
||||
foreach ( $this->resources as $resource ) {
|
||||
$result[] = [
|
||||
|
@ -163,7 +177,7 @@ class Server {
|
|||
return $result;
|
||||
}
|
||||
|
||||
private function readResource( $uri ) {
|
||||
private function read_resource( $uri ) {
|
||||
// Find the resource by URI
|
||||
$resource = null;
|
||||
foreach ( $this->resources as $r ) {
|
||||
|
@ -174,61 +188,64 @@ class Server {
|
|||
}
|
||||
|
||||
if ( ! $resource ) {
|
||||
throw new Exception( "Resource not found." );
|
||||
throw new Exception( 'Resource not found.' );
|
||||
}
|
||||
|
||||
// Access the resource data (replace with your actual data access logic)
|
||||
$data = $this->getResourceData( $resource );
|
||||
$data = $this->get_resource_data( $resource );
|
||||
|
||||
// Determine if it's text or binary
|
||||
$isBinary = isset( $resource['mimeType'] ) && ! str_starts_with( $resource['mimeType'], 'text/' );
|
||||
$is_binary = isset( $resource['mimeType'] ) && ! str_starts_with( $resource['mimeType'], 'text/' );
|
||||
|
||||
return [
|
||||
'uri' => $resource['uri'],
|
||||
'mimeType' => $resource['mimeType'] ?? null,
|
||||
( $isBinary ? 'blob' : 'text' ) => $data,
|
||||
'uri' => $resource['uri'],
|
||||
'mimeType' => $resource['mimeType'] ?? null,
|
||||
( $is_binary ? 'blob' : 'text' ) => $data,
|
||||
];
|
||||
}
|
||||
|
||||
private function getResourceData( $resource ) {
|
||||
private function get_resource_data( $mcp_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'] );
|
||||
if ( isset( $mcp_resource['filePath'] ) ) {
|
||||
return file_get_contents( $mcp_resource['filePath'] );
|
||||
}
|
||||
|
||||
// Example: If the resource is in the $data array, return the data.
|
||||
if ( isset( $resource['dataKey'] ) ) {
|
||||
return $this->data[ $resource['dataKey'] ];
|
||||
if ( isset( $mcp_resource['dataKey'] ) ) {
|
||||
return $this->data[ $mcp_resource['dataKey'] ];
|
||||
}
|
||||
|
||||
//... other data access logic...
|
||||
|
||||
throw new Exception( "Unable to access resource data." );
|
||||
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)
|
||||
// TODO: use a dedicated JSON schema validator library
|
||||
private function validate_input( $input, $schema ): array {
|
||||
$errors = [];
|
||||
foreach ( $schema['properties'] ?? [] as $paramName => $paramSchema ) {
|
||||
if ( isset( $paramSchema['required'] ) && $paramSchema['required'] === true && ! isset( $input[ $paramName ] ) ) {
|
||||
$errors[] = $paramName . " is required";
|
||||
foreach ( $schema['properties'] ?? [] as $param_name => $param_schema ) {
|
||||
if ( isset( $param_schema['required'] ) && true === $param_schema['required'] && ! isset( $input[ $param_name ] ) ) {
|
||||
$errors[] = $param_name . ' 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.";
|
||||
if ( isset( $input[ $param_name ], $param_schema['type'] ) ) {
|
||||
$input_type = gettype( $input[ $param_name ] );
|
||||
if ( $input_type !== $param_schema['type'] ) {
|
||||
$errors[] = $param_name . ' must be of type ' . $param_schema['type'] . ' but ' . $input_type . ' was given.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [ 'valid' => empty( $errors ), 'errors' => $errors ];
|
||||
return [
|
||||
'valid' => empty( $errors ),
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
private function handleGetRequest( $path, $params ) {
|
||||
private function handle_get_request( $path, $params ) {
|
||||
$parts = explode( '/', ltrim( $path, '/' ) );
|
||||
$resource = $parts[0];
|
||||
$id = $params['id'] ?? null; // Simplified parameter handling
|
||||
|
@ -236,42 +253,47 @@ class Server {
|
|||
if ( isset( $this->data[ $resource ] ) ) {
|
||||
$data = $this->data[ $resource ];
|
||||
|
||||
if ( $id !== null ) {
|
||||
if ( null !== $id ) {
|
||||
foreach ( $data as $item ) {
|
||||
if ( $item['id'] == $id ) {
|
||||
if ( $item['id'] === $id ) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
throw new Exception( 'Resource not found' );
|
||||
} else {
|
||||
return $data;
|
||||
}
|
||||
} else {
|
||||
throw new Exception( 'Resource not found' );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
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,
|
||||
private function create_success_response( $id, $result ): false|string {
|
||||
return json_encode(
|
||||
[
|
||||
'jsonrpc' => '2.0',
|
||||
'result' => $result,
|
||||
'id' => $id,
|
||||
],
|
||||
'id' => $id,
|
||||
] );
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
public function processRequest( $requestData ): false|string {
|
||||
return $this->handleRequest( $requestData );
|
||||
private function create_error_response( $id, $message, $code ): false|string {
|
||||
return json_encode(
|
||||
[
|
||||
'jsonrpc' => '2.0',
|
||||
'error' => [
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
],
|
||||
'id' => $id,
|
||||
],
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
public function process_request( $request_data ): false|string {
|
||||
return $this->handle_request( $request_data );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
11
tests/MCP/Client/ClientTest.php
Normal file
11
tests/MCP/Client/ClientTest.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace WP_CLI\AiCommand\Tests\MCP\Client;
|
||||
|
||||
use WP_CLI\Tests\TestCase;
|
||||
|
||||
class ClientTest extends TestCase {
|
||||
public function test_runs(): void {
|
||||
$this->assertTrue( true );
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue