Skip to content

Commit 75d4f49

Browse files
Merge pull request #45 from Automattic/experimental/mcp-rest-api-crud
Implement REST API CRUD Tools and Update Settings
2 parents aa8716a + 684a7b7 commit 75d4f49

File tree

8 files changed

+364
-45
lines changed

8 files changed

+364
-45
lines changed

includes/Admin/Settings.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ public function enqueue_scripts( string $hook ): void {
127127
'enableUpdateToolsDescription' => __( 'Allow update operations via tools.', 'wordpress-mcp' ),
128128
'enableDeleteTools' => __( 'Enable Delete Tools', 'wordpress-mcp' ),
129129
'enableDeleteToolsDescription' => __( '⚠️ CAUTION: Allow deletion operations via tools.', 'wordpress-mcp' ),
130+
'enableRestApiCrudTools' => __( '🧪 Enable REST API CRUD Tools (EXPERIMENTAL)', 'wordpress-mcp' ),
131+
'enableRestApiCrudToolsDescription' => __( '⚠️ EXPERIMENTAL FEATURE: Enable or disable the generic REST API CRUD tools for accessing WordPress endpoints. This is experimental functionality that may change or be removed in future versions. When enabled, all tools that are a rest_alias or have the disabled_by_rest_crud flag will be disabled.', 'wordpress-mcp' ),
130132
'saveSettings' => __( 'Save Settings', 'wordpress-mcp' ),
131133
'settingsSaved' => __( 'Settings saved successfully!', 'wordpress-mcp' ),
132134
'settingsError' => __( 'Error saving settings. Please try again.', 'wordpress-mcp' ),
@@ -197,6 +199,12 @@ public function sanitize_settings( array $input ): array {
197199
$sanitized['enable_delete_tools'] = false;
198200
}
199201

202+
if ( isset( $input['enable_rest_api_crud_tools'] ) ) {
203+
$sanitized['enable_rest_api_crud_tools'] = (bool) $input['enable_rest_api_crud_tools'];
204+
} else {
205+
$sanitized['enable_rest_api_crud_tools'] = false;
206+
}
207+
200208
return $sanitized;
201209
}
202210

includes/Core/RegisterMcpTool.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ private function validate_arguments(): void {
186186
throw new InvalidArgumentException( 'The functionality type is required.' );
187187
}
188188

189-
// validate functionality type: must be one of 'create', 'read', 'update', 'delete'.
190-
$valid_types = array( 'create', 'read', 'update', 'delete' );
189+
// validate functionality type: must be one of 'create', 'read', 'update', 'delete', 'action'.
190+
$valid_types = array( 'create', 'read', 'update', 'delete', 'action' );
191191
if ( ! in_array( $this->args['type'], $valid_types, true ) ) {
192192
throw new InvalidArgumentException( 'The functionality type must be one of: ' . esc_html( implode( ', ', $valid_types ) ) );
193193
}

includes/Core/WpMcp.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Automattic\WordpressMcp\Tools\McpCustomPostTypesTools;
77
use Automattic\WordpressMcp\Tools\McpPostsTools;
88
use Automattic\WordpressMcp\Resources\McpGeneralSiteInfo;
9+
use Automattic\WordpressMcp\Tools\McpRestApiCrud;
910
use Automattic\WordpressMcp\Tools\McpSiteInfo;
1011
use Automattic\WordpressMcp\Tools\McpUsersTools;
1112
use Automattic\WordpressMcp\Tools\McpWooOrders;
@@ -189,6 +190,7 @@ private function init_default_tools(): void {
189190
new McpSettingsTools();
190191
new McpMediaTools();
191192
new McpCustomPostTypesTools();
193+
new McpRestApiCrud();
192194
}
193195

194196
/**
@@ -230,8 +232,8 @@ public static function instance(): WpMcp {
230232
*/
231233
private function is_tool_type_enabled( string $type ): bool {
232234

233-
// Read operations are always allowed if MCP is enabled.
234-
if ( 'read' === $type ) {
235+
// Read operations and action operations are always allowed if MCP is enabled.
236+
if ( 'read' === $type || 'action' === $type ) {
235237
return true;
236238
}
237239

@@ -257,14 +259,25 @@ private function is_tool_type_enabled( string $type ): bool {
257259
* @throws InvalidArgumentException If the tool name is not unique or if the tool type is disabled.
258260
*/
259261
public function register_tool( array $args ): void {
260-
261262
$is_tool_type_enabled = $this->is_tool_type_enabled( $args['type'] );
262263
$is_tool_enabled = $this->is_tool_enabled( $args['name'] );
263264

265+
// Check if REST API CRUD tools are enabled and this tool should be disabled
266+
$is_rest_api_crud_enabled = ! empty( $this->mcp_settings['enable_rest_api_crud_tools'] );
267+
$has_rest_alias = ! empty( $args['rest_alias'] );
268+
$has_disabled_flag = ! empty( $args['disabled_by_rest_crud'] );
269+
$is_disabled_by_rest_crud = $is_rest_api_crud_enabled && ( $has_rest_alias || $has_disabled_flag );
270+
264271
$args['tool_type_enabled'] = $is_tool_type_enabled;
265272
$args['tool_enabled'] = $is_tool_enabled;
273+
$args['disabled_by_rest_crud'] = $is_disabled_by_rest_crud;
266274

267275
$this->all_tools[] = $args;
276+
277+
// Skip actual registration if disabled by REST CRUD setting
278+
if ( $is_disabled_by_rest_crud ) {
279+
return;
280+
}
268281
// Check if the tool is enabled.
269282
if ( ! $is_tool_enabled || ! $is_tool_type_enabled ) {
270283
return; // Skip registration if tool is disabled.
@@ -292,6 +305,7 @@ public function register_tool( array $args ): void {
292305
unset( $args['callback'] );
293306
unset( $args['permission_callback'] );
294307
unset( $args['rest_alias'] );
308+
unset( $args['disabled_by_rest_crud'] );
295309
$this->tools[] = $args;
296310
}
297311

includes/Tools/McpCustomPostTypesTools.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public function register_tools(): void {
5555
'type' => 'read',
5656
'callback' => array( $this, 'search_custom_post_types' ),
5757
'permission_callback' => array( $this, 'search_custom_post_types_permission_callback' ),
58+
'disabled_by_rest_crud' => true,
5859
'inputSchema' => array(
5960
'type' => 'object',
6061
'properties' => array(
@@ -104,6 +105,7 @@ public function register_tools(): void {
104105
'type' => 'read',
105106
'callback' => array( $this, 'get_custom_post_type' ),
106107
'permission_callback' => array( $this, 'get_custom_post_type_permission_callback' ),
108+
'disabled_by_rest_crud' => true,
107109
'inputSchema' => array(
108110
'type' => 'object',
109111
'properties' => array(
@@ -136,6 +138,7 @@ public function register_tools(): void {
136138
'type' => 'create',
137139
'callback' => array( $this, 'add_custom_post_type' ),
138140
'permission_callback' => array( $this, 'add_custom_post_type_permission_callback' ),
141+
'disabled_by_rest_crud' => true,
139142
'inputSchema' => array(
140143
'type' => 'object',
141144
'properties' => array(
@@ -183,6 +186,7 @@ public function register_tools(): void {
183186
'type' => 'update',
184187
'callback' => array( $this, 'update_custom_post_type' ),
185188
'permission_callback' => array( $this, 'update_custom_post_type_permission_callback' ),
189+
'disabled_by_rest_crud' => true,
186190
'inputSchema' => array(
187191
'type' => 'object',
188192
'properties' => array(
@@ -233,6 +237,7 @@ public function register_tools(): void {
233237
'type' => 'delete',
234238
'callback' => array( $this, 'delete_custom_post_type' ),
235239
'permission_callback' => array( $this, 'delete_custom_post_type_permission_callback' ),
240+
'disabled_by_rest_crud' => true,
236241
'inputSchema' => array(
237242
'type' => 'object',
238243
'properties' => array(

includes/Tools/McpRestApiCrud.php

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<?php //phpcs:ignore
2+
/**
3+
* Class McpWordPressRestApi
4+
*
5+
* Registers generic MCP tools for CRUD actions on any WordPress REST API endpoint.
6+
*
7+
* @package Automattic\WordpressMcp\Tools
8+
*/
9+
declare( strict_types=1 );
10+
11+
namespace Automattic\WordpressMcp\Tools;
12+
13+
use Automattic\WordpressMcp\Core\RegisterMcpTool;
14+
use WP_REST_Request;
15+
16+
/**
17+
* Class McpWordPressRestApi
18+
*
19+
* Registers generic MCP tools for CRUD actions on any WordPress REST API endpoint.
20+
*
21+
* @package Automattic\WordpressMcp\Tools
22+
*/
23+
class McpRestApiCrud {
24+
/**
25+
* Constructor.
26+
*/
27+
public function __construct() {
28+
add_action( 'wordpress_mcp_init', array( $this, 'register_tools' ) );
29+
}
30+
31+
/**
32+
* Register generic CRUD tools for a given REST API endpoint.
33+
*
34+
* Example usage: You can extend this to register tools for any custom endpoint.
35+
*/
36+
public function register_tools(): void {
37+
// Check if REST API CRUD tools are enabled in settings
38+
$settings = get_option( 'wordpress_mcp_settings', array() );
39+
if ( empty( $settings['enable_rest_api_crud_tools'] ) ) {
40+
return;
41+
}
42+
43+
// Example: Register CRUD tools for a custom endpoint '/wp/v2/example'.
44+
// To use for other endpoints, duplicate and adjust the route/method/name/description as needed.
45+
46+
new RegisterMcpTool(
47+
array(
48+
'name' => 'list_wordpress_rest_api_endpoints',
49+
'description' => 'List all available WordPress REST API endpoints and their supported HTTP methods. Use this first to discover what API endpoints are available before making specific calls.',
50+
'type' => 'read',
51+
'inputSchema' => array(
52+
'type' => 'object',
53+
'properties' => new \stdClass(),
54+
'required' => new \stdClass(),
55+
),
56+
'callback' => array( $this, 'get_available_tools' ),
57+
'permission_callback' => '__return_true',
58+
'annotations' => array(
59+
'title' => 'List REST API Endpoints',
60+
'readOnlyHint' => true,
61+
'openWorldHint' => false,
62+
),
63+
)
64+
);
65+
66+
new RegisterMcpTool(
67+
array(
68+
'name' => 'get_wordpress_rest_api_endpoint_schema',
69+
'description' => 'Get the complete schema and documentation for a specific WordPress REST API endpoint and HTTP method. Use this to understand what parameters are required and available for an endpoint before making calls.',
70+
'type' => 'read',
71+
'inputSchema' => array(
72+
'type' => 'object',
73+
'properties' => array(
74+
'route' => array(
75+
'type' => 'string',
76+
'description' => 'The REST API route (e.g., "/wp/v2/posts", "/wp/v2/users")'
77+
),
78+
'method' => array(
79+
'type' => 'string',
80+
'enum' => array( 'GET', 'POST', 'PATCH', 'DELETE' ),
81+
'description' => 'The HTTP method to get schema for'
82+
),
83+
),
84+
'required' => array( 'route', 'method' ),
85+
),
86+
'callback' => array( $this, 'get_tool_details' ),
87+
'permission_callback' => '__return_true',
88+
'annotations' => array(
89+
'title' => 'Get Endpoint Schema',
90+
'readOnlyHint' => true,
91+
'openWorldHint' => false,
92+
),
93+
)
94+
);
95+
96+
new RegisterMcpTool(
97+
array(
98+
'name' => 'call_wordpress_rest_api',
99+
'description' => 'Make a direct call to any WordPress REST API endpoint. Supports GET (read), POST (create), PATCH (update), and DELETE operations. Use this to interact with WordPress content like posts, pages, users, etc.',
100+
'type' => 'action',
101+
'inputSchema' => array(
102+
'type' => 'object',
103+
'properties' => array(
104+
'route' => array(
105+
'type' => 'string',
106+
'description' => 'The REST API route (e.g., "/wp/v2/posts", "/wp/v2/users/123")'
107+
),
108+
'method' => array(
109+
'type' => 'string',
110+
'enum' => array( 'GET', 'POST', 'PATCH', 'DELETE' ),
111+
'description' => 'The HTTP method: GET (read), POST (create), PATCH (update), DELETE (remove)'
112+
),
113+
'data' => array(
114+
'type' => 'object',
115+
'description' => 'Request body data for POST/PATCH requests. Not needed for GET/DELETE.'
116+
),
117+
),
118+
'required' => array( 'route', 'method' ),
119+
),
120+
'callback' => array( $this, 'handle_tool_run_request' ),
121+
'permission_callback' => '__return_true',
122+
'annotations' => array(
123+
'title' => 'Call REST API',
124+
'readOnlyHint' => false,
125+
'destructiveHint' => true,
126+
'idempotentHint' => false,
127+
'openWorldHint' => false,
128+
),
129+
)
130+
);
131+
}
132+
133+
/**
134+
* Handle a REST API request.
135+
*
136+
* @param array $data The request data.
137+
* @return array The response data.
138+
*/
139+
public function handle_tool_run_request( array $data ): array {
140+
$route = $data['route'];
141+
$method = $data['method'];
142+
$data = $data['data'];
143+
144+
// Get settings to check if operations are enabled
145+
$settings = get_option( 'wordpress_mcp_settings', array() );
146+
147+
// Check if the method is allowed based on settings
148+
switch ( $method ) {
149+
case 'DELETE':
150+
if ( empty( $settings['enable_delete_tools'] ) ) {
151+
return array(
152+
'error' => 'Delete operations are disabled in MCP settings.',
153+
'code' => 'operation_disabled',
154+
);
155+
}
156+
break;
157+
case 'POST':
158+
if ( empty( $settings['enable_create_tools'] ) ) {
159+
return array(
160+
'error' => 'Create operations are disabled in MCP settings.',
161+
'code' => 'operation_disabled',
162+
);
163+
}
164+
break;
165+
case 'PATCH':
166+
case 'PUT':
167+
if ( empty( $settings['enable_update_tools'] ) ) {
168+
return array(
169+
'error' => 'Update operations are disabled in MCP settings.',
170+
'code' => 'operation_disabled',
171+
);
172+
}
173+
break;
174+
}
175+
176+
$rest_request = new WP_REST_Request( $method, $route );
177+
$rest_request->set_body_params( $data );
178+
$response = rest_do_request( $rest_request );
179+
return $response->get_data();
180+
}
181+
182+
/**
183+
* Get all routes and methods from the WordPress REST API.
184+
*
185+
* @return array The routes and methods.
186+
*/
187+
public function get_available_tools(): array {
188+
// content.text.result[key]
189+
// get all routes and methods from the WordPress rest api.
190+
$routes = rest_get_server()->get_routes();
191+
foreach ( $routes as $route => $methods ) {
192+
foreach ( $methods as $the_methods ) {
193+
$result[] = array(
194+
'route' => $route,
195+
'method' => key( $the_methods['methods'] ),
196+
);
197+
}
198+
}
199+
return $result;
200+
}
201+
202+
/**
203+
* Get details of a WordPress REST API tool.
204+
*
205+
* @param array $data The request data.
206+
* @return array|null The response data.
207+
*/
208+
public function get_tool_details( array $data ): array {
209+
$route = $data['route'];
210+
$method = $data['method'];
211+
212+
$routes = rest_get_server()->get_routes();
213+
foreach ( $routes as $route => $methods ) {
214+
foreach ( $methods as $method => $args ) {
215+
if ( $route === $route && $method === $method ) {
216+
return $args;
217+
}
218+
}
219+
}
220+
return array();
221+
}
222+
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "wordpress-mcp",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "A plugin to integrate WordPress with Model Context Protocol (MCP), providing AI-accessible interfaces to WordPress data and functionality through standardized tools, resources, and prompts. Enables AI assistants to interact with posts, users, site settings, and WooCommerce data.",
55
"keywords": [
66
"wordpress",
@@ -27,7 +27,7 @@
2727
"lint:js": "wp-scripts lint-js",
2828
"packages-update": "wp-scripts packages-update",
2929
"plugin-zip": "wp-scripts plugin-zip",
30-
"plugin-zip:build": "composer install --no-dev && npm run build && npm run plugin-zip",
30+
"plugin-zip:build": "composer install --no-dev && pnpm run build && pnpm run plugin-zip",
3131
"clean": "rm -rf build"
3232
},
3333
"dependencies": {

0 commit comments

Comments
 (0)