Skip to content

Commit a3b2338

Browse files
committed
Merge branch 'feature/create_post' of https://github.com/swissspidy/ai-command into feature/create_post
2 parents 9bb5710 + cfc8fcf commit a3b2338

File tree

4 files changed

+230
-36
lines changed

4 files changed

+230
-36
lines changed

src/AiCommand.php

Lines changed: 180 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use WP_CLI;
66
use WP_CLI_Command;
7+
use WP_Community_Events;
8+
use WP_Error;
79

810
/**
911
*
@@ -44,6 +46,46 @@ class AiCommand extends WP_CLI_Command {
4446
*/
4547
public function __invoke( $args, $assoc_args ) {
4648
$server = new MCP\Server();
49+
$client = new MCP\Client($server);
50+
51+
$this->register_tools($server, $client);
52+
53+
$this->register_resources($server);
54+
55+
$result = $client->call_ai_service_with_prompt( $args[0] );
56+
57+
WP_CLI::success( $result );
58+
}
59+
60+
// Register tools for AI processing
61+
private function register_tools($server, $client) {
62+
$server->register_tool(
63+
[
64+
'name' => 'list_tools',
65+
'description' => 'Lists all available tools with their descriptions.',
66+
'inputSchema' => [
67+
'type' => 'object', // Object type for input
68+
'properties' => [
69+
'placeholder' => [
70+
'type' => 'integer',
71+
'description' => '',
72+
]
73+
],
74+
'required' => [], // No required fields
75+
],
76+
'callable' => function () use ($server) {
77+
// Get all capabilities
78+
$capabilities = $server->get_capabilities();
79+
80+
// Prepare a list of tools with their descriptions
81+
$tool_list = 'Return this to the user as a bullet list with each tool name and description on a new line. \n\n';
82+
$tool_list .= print_r($capabilities['methods'], true);
83+
84+
// Return the formatted string of tools with descriptions
85+
return $tool_list;
86+
},
87+
]
88+
);
4789

4890
$map_rest_to_mcp = new MapRESTtoMCP(
4991
require __DIR__ . '/../settings/rest-routes.php'
@@ -89,12 +131,6 @@ public function __invoke( $args, $assoc_args ) {
89131
]
90132
);
91133

92-
$client = new MCP\Client( $server );
93-
$result = $client->call_ai_service_with_prompt( $args[0] );
94-
95-
WP_CLI::success( $result );
96-
return;
97-
98134
$server->register_tool(
99135
[
100136
'name' => 'calculate_total',
@@ -122,6 +158,37 @@ public function __invoke( $args, $assoc_args ) {
122158
]
123159
);
124160

161+
162+
163+
// Register tool to retrieve last N posts in JSON format.
164+
$server->register_tool([
165+
'name' => 'list_posts',
166+
'description' => 'Retrieves the last N posts.',
167+
'inputSchema' => [
168+
'type' => 'object',
169+
'properties' => [
170+
'count' => [
171+
'type' => 'integer',
172+
'description' => 'The number of posts to retrieve.',
173+
],
174+
],
175+
'required' => ['count'],
176+
],
177+
'callable' => function ($params) {
178+
$query = new \WP_Query([
179+
'posts_per_page' => $params['count'],
180+
'post_status' => 'publish',
181+
]);
182+
$posts = [];
183+
while ($query->have_posts()) {
184+
$query->the_post();
185+
$posts[] = ['title' => get_the_title(), 'content' => get_the_content()];
186+
}
187+
wp_reset_postdata();
188+
return $posts;
189+
},
190+
]);
191+
125192
$server->register_tool(
126193
[
127194
'name' => 'greet',
@@ -142,29 +209,6 @@ public function __invoke( $args, $assoc_args ) {
142209
]
143210
);
144211

145-
// Register resources:
146-
$server->register_resource(
147-
[
148-
'name' => 'users',
149-
'uri' => 'data://users',
150-
'description' => 'List of users',
151-
'mimeType' => 'application/json',
152-
'dataKey' => 'users', // This tells getResourceData() to look in the $data array
153-
]
154-
);
155-
156-
$server->register_resource(
157-
[
158-
'name' => 'product_catalog',
159-
'uri' => 'file://./products.json',
160-
'description' => 'Product catalog',
161-
'mimeType' => 'application/json',
162-
'filePath' => './products.json', // This tells getResourceData() to read from a file
163-
]
164-
);
165-
166-
$client = new MCP\Client( $server );
167-
168212
$server->register_tool(
169213
[
170214
'name' => 'generate_image',
@@ -185,9 +229,114 @@ public function __invoke( $args, $assoc_args ) {
185229
]
186230
);
187231

188-
$result = $client->call_ai_service_with_prompt( $args[0] );
232+
$server->register_tool(
233+
[
234+
'name' => 'fetch_wp_community_events',
235+
'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.',
236+
'inputSchema' => [
237+
'type' => 'object',
238+
'properties' => [
239+
'location' => [
240+
'type' => 'string',
241+
'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).',
242+
],
243+
],
244+
'required' => [ 'location' ], // We only require the location
245+
],
246+
'callable' => function ( $params ) {
247+
// Default user ID is 0
248+
$user_id = 0;
189249

190-
WP_CLI::success( $result );
250+
// Get the location from the parameters (already supplied in the prompt)
251+
$location_input = strtolower( trim( $params['location'] ) );
252+
253+
// Manually include the WP_Community_Events class if it's not loaded
254+
if ( ! class_exists( 'WP_Community_Events' ) ) {
255+
require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php';
256+
}
257+
258+
// Determine location for the WP_Community_Events class
259+
$location = null;
260+
if ( $location_input !== 'near me' ) {
261+
// Provide city name (WP will resolve coordinates)
262+
$location = [
263+
'description' => $location_input,
264+
];
265+
}
266+
267+
// Instantiate WP_Community_Events with user ID (0) and optional location
268+
$events_instance = new WP_Community_Events( $user_id, $location );
269+
270+
// Get events from WP_Community_Events
271+
$events = $events_instance->get_events($location_input);
272+
273+
// Check for WP_Error
274+
if ( is_wp_error( $events ) ) {
275+
return [ 'error' => $events->get_error_message() ];
276+
}
277+
278+
// If no events found
279+
if ( empty( $events['events'] ) ) {
280+
return [ 'message' => 'No events found near ' . ( $location_input === 'near me' ? 'your location' : $location_input ) ];
281+
}
282+
283+
// Format and return the events correctly
284+
$formatted_events = array_map( function ( $event ) {
285+
// Log event details to ensure properties are accessible
286+
error_log( 'Event details: ' . print_r( $event, true ) );
287+
288+
// Initialize a formatted event string
289+
$formatted_event = '';
191290

291+
// Format event title
292+
if ( isset( $event['title'] ) ) {
293+
$formatted_event .= $event['title'] . "\n";
294+
}
295+
296+
// Format the date nicely
297+
$formatted_event .= ' - Date: ' . ( isset( $event['date'] ) ? date( 'F j, Y g:i A', strtotime( $event['date'] ) ) : 'No date available' ) . "\n";
298+
299+
// Format the location
300+
if ( isset( $event['location']['location'] ) ) {
301+
$formatted_event .= ' - Location: ' . $event['location']['location'] . "\n";
302+
}
303+
304+
// Format the event URL
305+
$formatted_event .= isset( $event['url'] ) ? ' - URL: ' . $event['url'] . "\n" : '';
306+
307+
return $formatted_event;
308+
}, $events['events'] );
309+
310+
// Combine the formatted events into a single string
311+
$formatted_events_output = implode("\n", $formatted_events);
312+
313+
// Return the formatted events string
314+
return [
315+
'message' => "OK. I found " . count($formatted_events) . " WordPress events near " . ( $location_input === 'near me' ? 'your location' : $location_input ) . ":\n\n" . $formatted_events_output
316+
];
317+
},
318+
]
319+
);
320+
}
321+
322+
// Register resources for AI access
323+
private function register_resources($server) {
324+
// Register Users resource
325+
$server->register_resource([
326+
'name' => 'users',
327+
'uri' => 'data://users',
328+
'description' => 'List of users',
329+
'mimeType' => 'application/json',
330+
'dataKey' => 'users', // Data will be fetched from 'users'
331+
]);
332+
333+
// Register Product Catalog resource
334+
$server->register_resource([
335+
'name' => 'product_catalog',
336+
'uri' => 'file://./products.json',
337+
'description' => 'Product catalog',
338+
'mimeType' => 'application/json',
339+
'filePath' => './products.json', // Data will be fetched from products.json
340+
]);
192341
}
193342
}

src/MCP/Client.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ static function () {
122122

123123
// See https://github.com/felixarntz/ai-services/blob/main/docs/Accessing-AI-Services-in-PHP.md for further processing.
124124

125-
WP_CLI::log( "Generated image: $image_url" );
125+
WP_CLI::debug( "Generated image: $image_url", 'ai' );
126126

127127
return $image_url;
128128
}
@@ -173,12 +173,19 @@ static function () {
173173
]
174174
);
175175

176-
\WP_CLI::debug( print_r($contents, true), 'mcp_server_raw' );
176+
\WP_CLI::debug( 'Making request...' . print_r( $contents, true ), 'ai' );
177+
178+
if ( $service->get_service_slug() === 'openai' ) {
179+
$model = 'gpt-4o';
180+
} else {
181+
$model = 'gemini-2.0-flash';
182+
}
177183

178184
$candidates = $service
179185
->get_model(
180186
[
181187
'feature' => 'text-generation',
188+
'model' => $model,
182189
'tools' => $tools,
183190
'capabilities' => [
184191
AI_Capability::MULTIMODAL_INPUT,
@@ -199,6 +206,10 @@ static function () {
199206
} elseif ( $part instanceof Function_Call_Part ) {
200207
$function_result = $this->{$part->get_name()}( $part->get_args() );
201208

209+
// Capture the function name here
210+
$function_name = $part->get_name();
211+
echo "Output generated with the '$function_name' tool:\n"; // Log the function name
212+
202213
// Odd limitation of add_function_response_part().
203214
if ( ! is_array( $function_result ) ) {
204215
$function_result = [ $function_result ];
@@ -221,7 +232,17 @@ static function () {
221232
return $this->call_ai_service( $new_contents );
222233
}
223234

224-
return $text;
235+
// Keep the session open to continue chatting.
236+
237+
WP_CLI::line( $text );
238+
239+
$response = \cli\prompt( '', false, '' );
240+
241+
$parts = new Parts();
242+
$parts->add_text_part( $response );
243+
$content = new Content( Content_Role::USER, $parts );
244+
$new_contents[] = $content;
245+
return $this->call_ai_service( $new_contents );
225246
} catch ( Exception $e ) {
226247
WP_CLI::error( $e->getMessage() );
227248
}

src/MCP/Server.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace WP_CLI\AiCommand\MCP;
44

55
use Exception;
6+
use WP_CLI;
67
use InvalidArgumentException;
78

89
class Server {
@@ -49,6 +50,12 @@ public function register_tool( array $tool_definition ): void {
4950
$description = $tool_definition['description'] ?? null;
5051
$input_schema = $tool_definition['inputSchema'] ?? null;
5152

53+
// TODO: This is a temporary limit.
54+
if ( count( $this->tools ) >= 128 ) {
55+
WP_CLI::debug( 'Too many tools, max is 128', 'tools' );
56+
return;
57+
}
58+
5259
$this->tools[ $name ] = [
5360
'name' => $name,
5461
'callable' => $callable,

src/MapRESTtoMCP.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ protected function is_route_allowed( $route ) {
8989
}
9090

9191
public function map_rest_to_mcp( Server $mcp_server ) {
92+
/**
93+
* @var \WP_REST_Server $server
94+
*/
9295
$server = rest_get_server();
9396
$routes = $server->get_routes();
9497

@@ -160,7 +163,7 @@ protected function generate_description( RouteInformation $information ) : str
160163
return $verb . ' ' . $determiner . ' ' . $title;
161164
}
162165

163-
protected function rest_callable( $inputs, $route, $method_name, $server ) {
166+
protected function rest_callable( $inputs, $route, $method_name, \WP_REST_Server $server ) {
164167
preg_match_all( '/\(?P<(\w+)>/', $route, $matches );
165168

166169
foreach( $matches[1] as $match ) {
@@ -183,6 +186,20 @@ protected function rest_callable( $inputs, $route, $method_name, $server ) {
183186
*/
184187
$response = $server->dispatch( $request );
185188

186-
return $server->response_to_data( $response, false );
189+
$data = $server->response_to_data( $response, false );
190+
191+
if( isset( $data[0]['slug'] ) ) {
192+
$debug_data = 'Result List: ';
193+
foreach ( $data as $item ) {
194+
$debug_data .= $item['id'] . '=>' . $item['slug'] . ', ';
195+
}
196+
} elseif( isset( $data['slug'] ) ) {
197+
$debug_data = 'Result: ' . $data['id'] . ' ' . $data['slug'];
198+
} else {
199+
$debug_data = 'Unknown format';
200+
}
201+
WP_CLI::debug( $debug_data, 'mcp_server' );
202+
203+
return $data;
187204
}
188205
}

0 commit comments

Comments
 (0)