Skip to content

Commit dd1bc26

Browse files
authored
Merge pull request #10 from wp-forge/codex-upsert-tools
[codex] Apply upsert tool naming
2 parents 602b850 + a1395af commit dd1bc26

7 files changed

Lines changed: 78 additions & 61 deletions

File tree

README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,15 @@ WordPress 6.9+ provides the core Abilities API. The MCP transport is provided by
6060
| --- | --- |
6161
| `wp-forge-users-search` | Search and filter WordPress users with pagination |
6262
| `wp-forge-get-user` | Get a WordPress user by ID |
63-
| `wp-forge-add-user` | Add a new WordPress user |
64-
| `wp-forge-update-user` | Update a WordPress user by ID |
63+
| `wp-forge-save-user` | Create or update a WordPress user |
6564
| `wp-forge-delete-user` | Delete a WordPress user by ID |
6665

6766
#### Settings
6867

6968
| Tool | Description |
7069
| --- | --- |
7170
| `wp-forge-get-general-settings` | Get WordPress general site settings |
72-
| `wp-forge-update-general-settings` | Update WordPress general site settings |
71+
| `wp-forge-save-general-settings` | Save WordPress general site settings |
7372

7473
#### Site Info
7574

@@ -93,7 +92,7 @@ WordPress 6.9+ provides the core Abilities API. The MCP transport is provided by
9392
| --- | --- |
9493
| `wp-forge-list-options` | List WordPress options by search or prefix |
9594
| `wp-forge-get-option` | Get a WordPress option value by name |
96-
| `wp-forge-update-option` | Update a WordPress option value by name |
95+
| `wp-forge-save-option` | Create or update a WordPress option value by name |
9796
| `wp-forge-delete-option` | Delete a WordPress option by name |
9897

9998
#### Comments
@@ -102,8 +101,7 @@ WordPress 6.9+ provides the core Abilities API. The MCP transport is provided by
102101
| --- | --- |
103102
| `wp-forge-list-comments` | List WordPress comments with filtering and pagination |
104103
| `wp-forge-get-comment` | Get a WordPress comment by ID |
105-
| `wp-forge-add-comment` | Add a comment to a WordPress post |
106-
| `wp-forge-update-comment` | Update a WordPress comment by ID |
104+
| `wp-forge-save-comment` | Create or update a WordPress comment |
107105
| `wp-forge-delete-comment` | Delete a WordPress comment by ID |
108106
| `wp-forge-approve-comment` | Approve a WordPress comment by ID |
109107
| `wp-forge-spam-comment` | Mark a WordPress comment as spam by ID |

src/Abilities.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,26 @@ private function get_user( $id ) {
13271327
return $user ? $this->format_user( $user ) : Response::error( 'User not found.', 404 );
13281328
}
13291329

1330+
/**
1331+
* Save a user.
1332+
*
1333+
* @param array<string,mixed> $params Params.
1334+
* @return mixed
1335+
*/
1336+
private function save_user( $params ) {
1337+
if ( isset( $params['id'] ) ) {
1338+
return $this->update_user( (int) $params['id'], $params );
1339+
}
1340+
1341+
foreach ( array( 'username', 'email', 'password' ) as $required ) {
1342+
if ( ! isset( $params[ $required ] ) || '' === (string) $params[ $required ] ) {
1343+
return Response::error( 'save-user requires ' . $required . ' when creating a user.', 400 );
1344+
}
1345+
}
1346+
1347+
return $this->insert_user( $params );
1348+
}
1349+
13301350
/**
13311351
* Insert user.
13321352
*

src/Tools/CommentManagementTools.php

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,17 @@ private function add_comment_abilities() {
4141
return $this->get_comment_tool( (int) $params['id'] );
4242
}, true, 'moderate_comments' );
4343

44-
$this->add_ability( self::INTERNAL_PREFIX . 'add-comment', 'Add Comment', 'Add a comment to a WordPress post', $this->schema(
44+
$this->add_ability( self::INTERNAL_PREFIX . 'save-comment', 'Save Comment', 'Create or update a WordPress comment', $this->schema(
4545
array(
46-
'post_id' => $this->int_prop( 'Post ID.' ),
46+
'id' => $this->int_prop( 'Comment ID. Omit to create a new comment.' ),
47+
'post_id' => $this->int_prop( 'Post ID. Required when creating a comment.' ),
4748
'content' => $this->string_prop( 'Comment content.' ),
4849
'author_name' => $this->string_prop( 'Author name.' ),
4950
'author_email' => $this->string_prop( 'Author email.' ),
5051
'status' => $this->string_prop( 'Comment status.', 'hold' ),
51-
),
52-
array( 'post_id', 'content' )
53-
), function ( $params ) {
54-
return $this->add_comment_tool( $params );
55-
}, false, 'edit_posts' );
56-
57-
$this->add_ability( self::INTERNAL_PREFIX . 'update-comment', 'Update Comment', 'Update a WordPress comment by ID', $this->schema(
58-
array(
59-
'id' => $this->int_prop( 'Comment ID.' ),
60-
'content' => $this->string_prop( 'Comment content.' ),
61-
'status' => $this->string_prop( 'Comment status.' ),
62-
),
63-
array( 'id' )
52+
)
6453
), function ( $params ) {
65-
return $this->update_comment_tool( $params );
54+
return $this->save_comment_tool( $params );
6655
}, false, 'moderate_comments' );
6756

6857
$this->add_ability( self::INTERNAL_PREFIX . 'delete-comment', 'Delete Comment', 'Delete a WordPress comment by ID', $id_schema, function ( $params ) {
@@ -118,6 +107,26 @@ private function get_comment_tool( $id ) {
118107
return $comment ? $this->format_comment( $comment ) : Response::error( 'Comment not found.', 404 );
119108
}
120109

110+
/**
111+
* Save a comment.
112+
*
113+
* @param array<string,mixed> $params Params.
114+
* @return mixed
115+
*/
116+
private function save_comment_tool( $params ) {
117+
if ( isset( $params['id'] ) ) {
118+
return $this->update_comment_tool( $params );
119+
}
120+
121+
foreach ( array( 'post_id', 'content' ) as $required ) {
122+
if ( ! isset( $params[ $required ] ) || '' === (string) $params[ $required ] ) {
123+
return Response::error( 'save-comment requires ' . $required . ' when creating a comment.', 400 );
124+
}
125+
}
126+
127+
return $this->add_comment_tool( $params );
128+
}
129+
121130
/**
122131
* Add a comment.
123132
*

src/Tools/OptionManagementTools.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ private function add_option_abilities() {
4444
return $this->get_option_tool( $params['option_name'] );
4545
}, true, 'manage_options' );
4646

47-
$this->add_ability( self::INTERNAL_PREFIX . 'update-option', 'Update Option', 'Update a WordPress option value by name', $this->schema(
47+
$this->add_ability( self::INTERNAL_PREFIX . 'save-option', 'Save Option', 'Create or update a WordPress option value by name', $this->schema(
4848
array(
4949
'option_name' => $this->string_prop( 'Option name.' ),
5050
'value' => array( 'description' => 'Option value.', 'type' => array( 'string', 'number', 'integer', 'boolean', 'array', 'object', 'null' ) ),

src/Tools/SiteManagementTools.php

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,21 @@ private function add_site_abilities() {
3535
$user_get_schema = $this->schema( array( 'id' => $this->int_prop( 'User ID.' ) ), array( 'id' ) );
3636
$user_write_schema = $this->schema(
3737
array(
38+
'id' => $this->int_prop( 'User ID. Omit to create a new user.' ),
3839
'username' => $this->string_prop( 'Username.' ),
39-
'email' => $this->string_prop( 'Email address.' ),
40-
'password' => $this->string_prop( 'Password.' ),
40+
'email' => $this->string_prop( 'Email address. Required when creating a user.' ),
41+
'password' => $this->string_prop( 'Password. Required when creating a user.' ),
4142
'first_name' => $this->string_prop( 'First name.' ),
4243
'last_name' => $this->string_prop( 'Last name.' ),
4344
'role' => $this->string_prop( 'Role.' ),
44-
),
45-
array( 'username', 'email', 'password' )
45+
)
4646
);
47-
$user_update_schema = $user_write_schema;
48-
$user_update_schema['properties']['id'] = $this->int_prop( 'User ID.' );
49-
$user_update_schema['required'] = array( 'id' );
5047

5148
$this->add_ability( self::INTERNAL_PREFIX . 'get-user', 'Get User', 'Get a WordPress user by ID', $user_get_schema, function ( $params ) {
5249
return $this->get_user( (int) $params['id'] );
5350
}, true, 'list_users' );
54-
$this->add_ability( self::INTERNAL_PREFIX . 'add-user', 'Add User', 'Add a new WordPress user', $user_write_schema, function ( $params ) {
55-
return $this->insert_user( $params );
56-
}, false, 'create_users' );
57-
$this->add_ability( self::INTERNAL_PREFIX . 'update-user', 'Update User', 'Update a WordPress user by ID', $user_update_schema, function ( $params ) {
58-
return $this->update_user( (int) $params['id'], $params );
51+
$this->add_ability( self::INTERNAL_PREFIX . 'save-user', 'Save User', 'Create or update a WordPress user', $user_write_schema, function ( $params ) {
52+
return $this->save_user( $params );
5953
}, false, 'edit_users' );
6054
$this->add_ability( self::INTERNAL_PREFIX . 'delete-user', 'Delete User', 'Delete a WordPress user by ID', $user_get_schema, function ( $params ) {
6155
return $this->delete_user( (int) $params['id'] );
@@ -76,7 +70,7 @@ private function add_site_abilities() {
7670
$this->add_ability( self::INTERNAL_PREFIX . 'get-general-settings', 'Get General Settings', 'Get WordPress general site settings', $this->schema(), function () {
7771
return $this->get_general_settings();
7872
}, true, 'manage_options' );
79-
$this->add_ability( self::INTERNAL_PREFIX . 'update-general-settings', 'Update General Settings', 'Update WordPress general site settings', $settings_schema, function ( $params ) {
73+
$this->add_ability( self::INTERNAL_PREFIX . 'save-general-settings', 'Save General Settings', 'Save WordPress general site settings', $settings_schema, function ( $params ) {
8074
return $this->update_general_settings( $params );
8175
}, false, 'manage_options' );
8276
$this->add_ability( self::INTERNAL_PREFIX . 'get-site-info', 'Get Site Info', 'Get detailed site information', $this->schema(), function () {

tests/integration/mcp-endpoint.mjs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,10 @@ const expectedTools = [
192192
'wp-forge-search-media',
193193
'wp-forge-users-search',
194194
'wp-forge-get-user',
195-
'wp-forge-add-user',
196-
'wp-forge-update-user',
195+
'wp-forge-save-user',
197196
'wp-forge-delete-user',
198197
'wp-forge-get-general-settings',
199-
'wp-forge-update-general-settings',
198+
'wp-forge-save-general-settings',
200199
'wp-forge-get-site-info',
201200
'wp-forge-list-plugins',
202201
'wp-forge-install-plugin',
@@ -209,12 +208,11 @@ const expectedTools = [
209208
'wp-forge-delete-theme',
210209
'wp-forge-list-options',
211210
'wp-forge-get-option',
212-
'wp-forge-update-option',
211+
'wp-forge-save-option',
213212
'wp-forge-delete-option',
214213
'wp-forge-list-comments',
215214
'wp-forge-get-comment',
216-
'wp-forge-add-comment',
217-
'wp-forge-update-comment',
215+
'wp-forge-save-comment',
218216
'wp-forge-delete-comment',
219217
'wp-forge-approve-comment',
220218
'wp-forge-spam-comment',
@@ -348,7 +346,7 @@ await expectSuccess('wp-forge-update-media', { id: mediaId, title: `MCP media up
348346
await expectSuccess('wp-forge-delete-media', { id: mediaId });
349347

350348
const usernameForTest = `mcp_user_${suffix}`.replace(/[^a-zA-Z0-9_]/g, '_');
351-
const userId = await expectSuccess('wp-forge-add-user', {
349+
const userId = await expectSuccess('wp-forge-save-user', {
352350
username: usernameForTest,
353351
email: `${usernameForTest}@example.com`,
354352
password: `mcp-password-${suffix}`,
@@ -357,11 +355,11 @@ const userId = await expectSuccess('wp-forge-add-user', {
357355
await expectSuccess('wp-forge-users-search', { search: usernameForTest, per_page: 5 });
358356
const user = await expectSuccess('wp-forge-get-user', { id: userId });
359357
assert(user.id === userId, 'wp-forge-get-user returned the wrong user');
360-
await expectSuccess('wp-forge-update-user', { id: userId, first_name: 'MCP', last_name: 'Integration' });
358+
await expectSuccess('wp-forge-save-user', { id: userId, first_name: 'MCP', last_name: 'Integration' });
361359
await expectSuccess('wp-forge-delete-user', { id: userId });
362360

363361
await expectSuccess('wp-forge-get-general-settings');
364-
await expectSuccess('wp-forge-update-general-settings', {});
362+
await expectSuccess('wp-forge-save-general-settings', {});
365363
await expectSuccess('wp-forge-get-site-info');
366364
await expectSuccess('wp-forge-get-active-theme');
367365

@@ -383,13 +381,13 @@ await expectSuccess('wp-forge-activate-theme', { stylesheet: activeTheme.stylesh
383381
await expectError('wp-forge-delete-theme', { stylesheet: activeTheme.stylesheet }, 400);
384382

385383
const optionName = `wp_forge_mcp_integration_${suffix}`;
386-
await expectSuccess('wp-forge-update-option', { option_name: optionName, value: { status: 'ok', suffix } });
384+
await expectSuccess('wp-forge-save-option', { option_name: optionName, value: { status: 'ok', suffix } });
387385
const option = await expectSuccess('wp-forge-get-option', { option_name: optionName });
388386
assert(option.exists === true, 'wp-forge-get-option did not find the test option');
389387
await expectSuccess('wp-forge-list-options', { name_prefix: 'wp_forge_mcp_integration_', per_page: 10 });
390388
await expectSuccess('wp-forge-delete-option', { option_name: optionName });
391389

392-
const commentId = await expectSuccess('wp-forge-add-comment', {
390+
const commentId = await expectSuccess('wp-forge-save-comment', {
393391
post_id: postId,
394392
content: `MCP comment ${suffix}`,
395393
author_name: 'MCP Integration',
@@ -399,7 +397,7 @@ const commentId = await expectSuccess('wp-forge-add-comment', {
399397
await expectSuccess('wp-forge-list-comments', { post_id: postId, per_page: 5 });
400398
const comment = await expectSuccess('wp-forge-get-comment', { id: commentId });
401399
assert(comment.id === commentId, 'wp-forge-get-comment returned the wrong comment');
402-
await expectSuccess('wp-forge-update-comment', { id: commentId, content: `MCP comment updated ${suffix}` });
400+
await expectSuccess('wp-forge-save-comment', { id: commentId, content: `MCP comment updated ${suffix}` });
403401
await expectSuccess('wp-forge-approve-comment', { id: commentId });
404402
await expectSuccess('wp-forge-spam-comment', { id: commentId });
405403
await expectSuccess('wp-forge-delete-comment', { id: commentId });
@@ -426,13 +424,13 @@ await expectSuccess('wp-forge-get-function-details', { route: '/wp/v2/types', me
426424
await expectSuccess('wp-forge-run-api-function', { route: '/wp/v2/types', method: 'GET' });
427425

428426
if (!username && !password) {
429-
await expectSuccess('wp-forge-update-option', { option_name: 'wp_forge_mcp_activity_log_enabled', value: '' });
427+
await expectSuccess('wp-forge-save-option', { option_name: 'wp_forge_mcp_activity_log_enabled', value: '' });
430428
const disabledSettingsPage = await getAdminSettingsPage();
431429
assert(disabledSettingsPage.includes('Enable MCP activity log'), 'Activity log setting was not displayed');
432430
assert(!disabledSettingsPage.includes('Filter Activity Log'), 'Activity log filters were displayed while disabled');
433431
assert(!disabledSettingsPage.includes('No MCP activity has been logged yet.'), 'Activity log empty state was displayed while disabled');
434432

435-
await expectSuccess('wp-forge-update-option', { option_name: 'wp_forge_mcp_activity_log_enabled', value: '1' });
433+
await expectSuccess('wp-forge-save-option', { option_name: 'wp_forge_mcp_activity_log_enabled', value: '1' });
436434
await expectSuccess('wp-forge-get-site-info');
437435
const settingsPage = await getAdminSettingsPage();
438436
assert(settingsPage.includes('wp-forge-get-site-info'), 'Activity log did not show a logged MCP tool call');

tests/run.php

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ function assert_same( $expected, $actual, $message ) {
171171
$all = $abilities->list_abilities();
172172
$names = array_column( $all, 'name' );
173173

174-
assert_same( 58, count( $all ), 'Expected the WordPress ability catalog.' );
174+
assert_same( 56, count( $all ), 'Expected the WordPress ability catalog.' );
175175
assert_true( in_array( 'wp-forge-search-content', $names, true ), 'Expected content search ability.' );
176176
assert_true( in_array( 'wp-forge-get-site-info', $names, true ), 'Expected site info ability.' );
177177
assert_true( in_array( 'wp-forge-run-api-function', $names, true ), 'Expected REST runner ability.' );
@@ -197,11 +197,10 @@ function assert_same( $expected, $actual, $message ) {
197197
'wp-forge-search-media',
198198
'wp-forge-users-search',
199199
'wp-forge-get-user',
200-
'wp-forge-add-user',
201-
'wp-forge-update-user',
200+
'wp-forge-save-user',
202201
'wp-forge-delete-user',
203202
'wp-forge-get-general-settings',
204-
'wp-forge-update-general-settings',
203+
'wp-forge-save-general-settings',
205204
'wp-forge-get-site-info',
206205
'wp-forge-list-plugins',
207206
'wp-forge-install-plugin',
@@ -214,12 +213,11 @@ function assert_same( $expected, $actual, $message ) {
214213
'wp-forge-delete-theme',
215214
'wp-forge-list-options',
216215
'wp-forge-get-option',
217-
'wp-forge-update-option',
216+
'wp-forge-save-option',
218217
'wp-forge-delete-option',
219218
'wp-forge-list-comments',
220219
'wp-forge-get-comment',
221-
'wp-forge-add-comment',
222-
'wp-forge-update-comment',
220+
'wp-forge-save-comment',
223221
'wp-forge-delete-comment',
224222
'wp-forge-approve-comment',
225223
'wp-forge-spam-comment',
@@ -256,7 +254,7 @@ function assert_same( $expected, $actual, $message ) {
256254

257255
$direct_tools = $abilities->list_tools();
258256
$direct_tool_names = array_column( $direct_tools, 'name' );
259-
assert_same( 58, count( $direct_tools ), 'Expected all abilities to be exposed as direct MCP tools.' );
257+
assert_same( 56, count( $direct_tools ), 'Expected all abilities to be exposed as direct MCP tools.' );
260258
assert_true( in_array( 'wp-forge-search-content', $direct_tool_names, true ), 'Direct tool list should include content search.' );
261259
assert_true( in_array( 'wp-forge-get-active-theme', $direct_tool_names, true ), 'Direct tool list should include active theme.' );
262260
assert_true( ! in_array( 'wp-forge-list-abilities', $direct_tool_names, true ), 'Gateway list tool should not be exposed.' );
@@ -298,11 +296,11 @@ function assert_same( $expected, $actual, $message ) {
298296
assert_same( array( 'page' ), array_column( $public_hierarchical_post_types['message']['post_types'], 'slug' ), 'Post type list should pass filters through to get_post_types().' );
299297

300298
$wp_ability_names = $abilities->get_wordpress_ability_names();
301-
assert_same( 58, count( $wp_ability_names ), 'Expected all abilities to be available for the MCP adapter.' );
299+
assert_same( 56, count( $wp_ability_names ), 'Expected all abilities to be available for the MCP adapter.' );
302300
assert_true( in_array( 'wp-forge/search-content', $wp_ability_names, true ), 'Adapter ability list should use WordPress ability names.' );
303301

304302
$abilities->register_wordpress_abilities();
305-
assert_same( 58, count( $registered_abilities ), 'Expected every ability to be registered with the WordPress Abilities API.' );
303+
assert_same( 56, count( $registered_abilities ), 'Expected every ability to be registered with the WordPress Abilities API.' );
306304
assert_true( isset( $registered_abilities['wp-forge/search-content'] ), 'Content search should be registered with the WordPress Abilities API.' );
307305
assert_same( 'Search and filter content for any registered post type', $registered_abilities['wp-forge/search-content']['description'], 'Registered ability should preserve descriptions.' );
308306
assert_same( true, $registered_abilities['wp-forge/search-content']['meta']['show_in_rest'], 'Registered abilities should be exposed through the Abilities REST API.' );
@@ -333,7 +331,7 @@ public function create_server() {
333331
assert_same( 'mcp', $adapter->args[1], 'Adapter server should keep the existing REST namespace.' );
334332
assert_same( 'wp-forge', $adapter->args[2], 'Adapter server should keep the existing REST route.' );
335333
assert_same( 'WordPress MCP', $adapter->args[3], 'Adapter server should preserve the server name.' );
336-
assert_same( 58, count( $adapter->args[9] ), 'Adapter server should expose every registered ability.' );
334+
assert_same( 56, count( $adapter->args[9] ), 'Adapter server should expose every registered ability.' );
337335
assert_true( in_array( 'wp-forge/search-content', $adapter->args[9], true ), 'Adapter server should expose content search.' );
338336

339337
echo 'Tests passed: ' . $tests_run . PHP_EOL;

0 commit comments

Comments
 (0)