testing setup, lint fixes

This commit is contained in:
Pascal Birchler 2025-03-10 14:47:11 +01:00
parent c75f95acd5
commit bf108dc87d
No known key found for this signature in database
GPG key ID: 0DECE73DD74E8B2F
11 changed files with 424 additions and 267 deletions

97
.github/workflows/testing.yml vendored Normal file
View 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
View file

@ -0,0 +1 @@
{"version":1,"defects":[],"times":{"AiCommand\\Tests\\MCP\\Client\\ClientTest::test_runs":0.001}}

View file

@ -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
View file

@ -0,0 +1,7 @@
default:
suites:
default:
contexts:
- WP_CLI\Tests\Context\FeatureContext
paths:
- features

View file

@ -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
View 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
View 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>

View file

@ -2,7 +2,6 @@

namespace WP_CLI\AiCommand;

use Exception;
use WP_CLI;
use WP_CLI_Command;

@ -45,14 +44,21 @@ class AiCommand extends WP_CLI_Command {
public function __invoke( $args, $assoc_args ) {
$server = new MCP\Server();

$server->registerTool( [
$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.' ],
'price' => [
'type' => 'integer',
'description' => 'The price of the item.',
],
'quantity' => [
'type' => 'integer',
'description' => 'The quantity of items.',
],
],
'required' => [ 'price', 'quantity' ],
],
@ -62,56 +68,71 @@ class AiCommand extends WP_CLI_Command {

return $price * $quantity;
},
] );
]
);

$server->registerTool( [
$server->register_tool(
[
'name' => 'greet',
'description' => 'Greets the user.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'name' => [ 'type' => 'string', 'description' => 'The name of the user.' ],
'name' => [
'type' => 'string',
'description' => 'The name of the user.',
],
],
'required' => [ 'name' ],
],
'callable' => function ( $params ) {
return "Hello, " . $params['name'] . "!";
return 'Hello, ' . $params['name'] . '!';
},
] );
]
);

// Register resources:
$server->registerResource( [
$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( [
$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( [
$server->register_tool(
[
'name' => 'generate_image',
'description' => 'Generates an image.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'prompt' => [ 'type' => 'string', 'description' => 'The prompt for generating the image.' ],
'prompt' => [
'type' => 'string',
'description' => 'The prompt for generating the image.',
],
],
'required' => [ 'prompt' ],
],
'callable' => function ( $params ) use ( $client ) {
return $client->get_image_from_ai_service( $params['prompt'] );
},
] );
]
);

$result = $client->call_ai_service_with_prompt( $args[0] );


View file

@ -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.
@ -73,7 +74,7 @@ class Client {
[
'capabilities' => [
AI_Capability::IMAGE_GENERATION,
]
],
]
);
$candidates = $service
@ -98,13 +99,13 @@ class Client {
$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,7 +211,7 @@ 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 );
$parts->add_function_response_part( $part->get_id(), $part->get_name(), $function_result );
$content = new Content( Content_Role::USER, $parts );
$new_contents[] = $content;
}

View file

@ -14,43 +14,59 @@ class Server {
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,
( $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' );
}
}

private function createSuccessResponse( $id, $result ): false|string {
return json_encode( [
return $data;
}

throw new Exception( 'Resource not found' );
}

private function create_success_response( $id, $result ): false|string {
return json_encode(
[
'jsonrpc' => '2.0',
'result' => $result,
'id' => $id,
] );
],
JSON_THROW_ON_ERROR
);
}

private function createErrorResponse( $id, $message, $code ): false|string {
return json_encode( [
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 processRequest( $requestData ): false|string {
return $this->handleRequest( $requestData );
public function process_request( $request_data ): false|string {
return $this->handle_request( $request_data );
}
}


View 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 );
}
}