@respira/wordpress-mcp-server 6.6.5 → 6.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -43,7 +43,7 @@ export class RespiraWordPressServer {
43
43
  allowedSites = null;
44
44
  /** Whether the plugin version warning has already been shown this session. */
45
45
  versionWarningShown = false;
46
- static MCP_SERVER_VERSION = '6.6.4';
46
+ static MCP_SERVER_VERSION = '6.11.1';
47
47
  /**
48
48
  * Normalize a tool name: respira_* → wordpress_* for switch dispatch.
49
49
  * Tracks whether the deprecated wordpress_* name was used.
@@ -103,6 +103,8 @@ export class RespiraWordPressServer {
103
103
 
104
104
  IMPORTANT: Always use Respira tools to read and edit WordPress content. NEVER access the database directly (SQL queries, wp_postmeta, wp_options), edit PHP files, or modify theme templates to change page content. Respira tools handle all content operations safely with snapshots, cache invalidation, and builder-native formats. Even for simple changes like updating a copyright year in a footer — use respira_find_element, not SQL.
105
105
 
106
+ CRITICAL TOOL CHOICE: For ANY in-page content edit (changing text, images, buttons, headings, links, colors, settings on existing widgets), use respira_find_element + respira_update_element. Do NOT use respira_update_page for content edits. respira_update_page replaces the ENTIRE page body and bypasses the page builder's structure — it produces "all code" pages where the builder treats every widget as a single text blob. respira_update_page is ONLY for: page title, slug, status, custom CSS, or full HTML replacement (e.g. "convert this Word doc into a page"). Telemetry across the user base shows respira_update_page being chosen ~13× more often than respira_update_element, but the right tool for "change the headline text" or "swap the hero image" is always respira_update_element.
107
+
106
108
  ## Tool naming
107
109
 
108
110
  Always use respira_* tool names (e.g. respira_update_page, respira_find_element). The wordpress_* names are deprecated aliases and will be removed in v6.0.
@@ -194,7 +196,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
194
196
  - Module names use et_pb_ prefix: et_pb_text, et_pb_blurb, et_pb_image, et_pb_button, et_pb_cta, et_pb_slider, etc.
195
197
  - Settings are shortcode attributes: [et_pb_text text_font="Roboto" text_text_color="#333333"]
196
198
  - Button uses url/url_new_window attributes (different from Divi 5's linkUrl/linkTarget)
197
- - IMPORTANT: When injecting content, set diviVersion to "4" in respira_inject_builder_content
199
+ - IMPORTANT: When injecting content, set divi_version to "4" in respira_inject_builder_content
198
200
  - Respira auto-detects whether the site runs Divi 4 or Divi 5 — check respira_get_builder_info for the version
199
201
  - Element-level operations work via shortcode tree parsing
200
202
 
@@ -666,7 +668,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
666
668
  type: 'number',
667
669
  description: 'Page number for pagination',
668
670
  },
669
- perPage: {
671
+ per_page: {
670
672
  type: 'number',
671
673
  description: 'Number of items per page',
672
674
  },
@@ -703,7 +705,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
703
705
  inputSchema: {
704
706
  type: 'object',
705
707
  properties: {
706
- originalId: {
708
+ original_id: {
707
709
  type: 'number',
708
710
  description: 'ID of the original page to duplicate',
709
711
  },
@@ -712,12 +714,12 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
712
714
  description: 'Optional suffix for the duplicated page title',
713
715
  },
714
716
  },
715
- required: ['originalId'],
717
+ required: ['original_id'],
716
718
  },
717
719
  },
718
720
  {
719
721
  name: 'wordpress_update_page',
720
- description: 'Update a page. When direct editing is enabled and you target an original page, Respira returns a confirmation_required preflight by default so you can choose live/original or duplicate.',
722
+ description: 'Update PAGE-LEVEL fields (title, status, slug, full HTML content replacement, custom CSS). For editing SPECIFIC elements WITHIN a page (text widgets, images, buttons, headings), use respira_update_element instead — it is element-aware, builder-aware, snapshot-safe, and works across all 12 page builders. Use respira_update_page only when you need to replace the entire content body, change the page title/slug/status, or set page-level metadata. When direct editing is enabled and you target an original page, Respira returns a confirmation_required preflight by default so you can choose live/original or duplicate.',
721
723
  inputSchema: {
722
724
  type: 'object',
723
725
  properties: {
@@ -741,7 +743,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
741
743
  type: 'string',
742
744
  description: 'URL slug (post_name). Example: "about-us" produces /about-us/.',
743
745
  },
744
- customCss: {
746
+ custom_css: {
745
747
  type: 'string',
746
748
  description: 'Custom CSS for the page. Auto-detects builder: Divi → _et_pb_custom_css, Elementor → _elementor_page_settings.custom_css.',
747
749
  },
@@ -749,7 +751,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
749
751
  type: 'object',
750
752
  description: 'Post meta data',
751
753
  },
752
- editTarget: {
754
+ edit_target: {
753
755
  type: 'string',
754
756
  description: 'Choose ask, live, or duplicate when the target is an original page.',
755
757
  enum: ['ask', 'live', 'duplicate'],
@@ -758,7 +760,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
758
760
  type: 'boolean',
759
761
  description: 'Backward-compatible alias for explicit live editing when direct editing is enabled.',
760
762
  },
761
- skipSecurityCheck: {
763
+ skip_security_check: {
762
764
  type: 'boolean',
763
765
  description: 'Advanced: bypass Respira content security checks and sanitization for this update. Use only with trusted content.',
764
766
  },
@@ -804,7 +806,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
804
806
  type: 'number',
805
807
  description: 'Page number',
806
808
  },
807
- perPage: {
809
+ per_page: {
808
810
  type: 'number',
809
811
  description: 'Items per page',
810
812
  },
@@ -841,7 +843,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
841
843
  inputSchema: {
842
844
  type: 'object',
843
845
  properties: {
844
- originalId: {
846
+ original_id: {
845
847
  type: 'number',
846
848
  description: 'Original post ID',
847
849
  },
@@ -850,7 +852,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
850
852
  description: 'Optional suffix',
851
853
  },
852
854
  },
853
- required: ['originalId'],
855
+ required: ['original_id'],
854
856
  },
855
857
  },
856
858
  {
@@ -910,19 +912,19 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
910
912
  type: 'object',
911
913
  description: 'Generic map of taxonomy slug to array of term ids/slugs for custom taxonomies (e.g. product_cat). Example: { "product_cat": [12, "featured"] }.',
912
914
  },
913
- featuredMedia: {
915
+ featured_media: {
914
916
  type: 'number',
915
917
  description: 'Attachment ID to set as the featured image. Pass 0 to clear.',
916
918
  },
917
- createMissingTerms: {
919
+ create_missing_terms: {
918
920
  type: 'boolean',
919
921
  description: 'When true, auto-create taxonomy terms that do not exist by name. Default false (strict: unknown slug returns 400).',
920
922
  },
921
- appendTerms: {
923
+ append_terms: {
922
924
  type: 'boolean',
923
925
  description: 'When true, append to existing taxonomy terms instead of replacing. Default false (replace).',
924
926
  },
925
- customCss: {
927
+ custom_css: {
926
928
  type: 'string',
927
929
  description: 'Custom CSS for the post. Auto-detects builder: Divi → _et_pb_custom_css, Elementor → _elementor_page_settings.custom_css.',
928
930
  },
@@ -930,7 +932,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
930
932
  type: 'object',
931
933
  description: 'Post meta data',
932
934
  },
933
- editTarget: {
935
+ edit_target: {
934
936
  type: 'string',
935
937
  description: 'Choose ask, live, or duplicate when the target is an original post.',
936
938
  enum: ['ask', 'live', 'duplicate'],
@@ -939,7 +941,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
939
941
  type: 'boolean',
940
942
  description: 'Backward-compatible alias for explicit live editing when direct editing is enabled.',
941
943
  },
942
- skipSecurityCheck: {
944
+ skip_security_check: {
943
945
  type: 'boolean',
944
946
  description: 'Advanced: bypass Respira content security checks and sanitization for this update. Use only with trusted content.',
945
947
  },
@@ -981,7 +983,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
981
983
  type: 'number',
982
984
  description: 'Page number',
983
985
  },
984
- perPage: {
986
+ per_page: {
985
987
  type: 'number',
986
988
  description: 'Items per page',
987
989
  },
@@ -1003,7 +1005,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1003
1005
  type: 'string',
1004
1006
  description: 'Filename for the uploaded file',
1005
1007
  },
1006
- mimeType: {
1008
+ mime_type: {
1007
1009
  type: 'string',
1008
1010
  description: 'MIME type of the file (e.g., image/jpeg, image/png, application/pdf)',
1009
1011
  },
@@ -1033,12 +1035,12 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1033
1035
  type: 'string',
1034
1036
  description: 'Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery. Use get_builder_info to discover the active builder first.',
1035
1037
  },
1036
- pageId: {
1038
+ page_id: {
1037
1039
  type: 'number',
1038
1040
  description: 'Page ID',
1039
1041
  },
1040
1042
  },
1041
- required: ['builder', 'pageId'],
1043
+ required: ['builder', 'page_id'],
1042
1044
  },
1043
1045
  readOnlyHint: true,
1044
1046
  },
@@ -1052,7 +1054,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1052
1054
  type: 'string',
1053
1055
  description: 'Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery. Use get_builder_info to discover the active builder first.',
1054
1056
  },
1055
- pageId: {
1057
+ page_id: {
1056
1058
  type: 'number',
1057
1059
  description: 'Page or post ID',
1058
1060
  },
@@ -1065,13 +1067,13 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1065
1067
  description: 'Maximum number of matches to return (default 10)',
1066
1068
  },
1067
1069
  },
1068
- required: ['builder', 'pageId'],
1070
+ required: ['builder', 'page_id'],
1069
1071
  },
1070
1072
  readOnlyHint: true,
1071
1073
  },
1072
1074
  {
1073
1075
  name: 'wordpress_inject_builder_content',
1074
- description: 'REPLACE (or append to) the entire page builder layout. WARNING: By default this REPLACES all existing content — use mode:"append" to add content without destroying existing elements. For editing a single module, use wordpress_update_module instead. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery. For Divi, diviVersion is required ("4" or "5").',
1076
+ description: 'REPLACE (or append to) the entire page builder layout. WARNING: By default this REPLACES all existing content — use mode:"append" to add content without destroying existing elements. For editing a single module, use wordpress_update_module instead. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery. For Divi, divi_version is required ("4" or "5").',
1075
1077
  inputSchema: {
1076
1078
  type: 'object',
1077
1079
  properties: {
@@ -1079,7 +1081,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1079
1081
  type: 'string',
1080
1082
  description: 'Builder identifier. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery.',
1081
1083
  },
1082
- pageId: {
1084
+ page_id: {
1083
1085
  type: 'number',
1084
1086
  description: 'Page ID',
1085
1087
  },
@@ -1092,18 +1094,18 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1092
1094
  description: 'replace (default): OVERWRITES all existing page content. append: adds new content after existing elements, preserving the current layout.',
1093
1095
  enum: ['replace', 'append'],
1094
1096
  },
1095
- editTarget: {
1097
+ edit_target: {
1096
1098
  type: 'string',
1097
1099
  description: 'When editing an original, choose ask, live, or duplicate. Defaults to ask when direct editing is enabled.',
1098
1100
  enum: ['ask', 'live', 'duplicate'],
1099
1101
  },
1100
- diviVersion: {
1102
+ divi_version: {
1101
1103
  type: 'string',
1102
1104
  description: 'Divi generation hint ("4" for shortcode format, "5" for block format). Required for all Divi inject calls.',
1103
1105
  enum: ['4', '5'],
1104
1106
  },
1105
1107
  },
1106
- required: ['builder', 'pageId', 'content'],
1108
+ required: ['builder', 'page_id', 'content'],
1107
1109
  },
1108
1110
  idempotentHint: true,
1109
1111
  },
@@ -1117,11 +1119,11 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1117
1119
  type: 'string',
1118
1120
  description: 'Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery.',
1119
1121
  },
1120
- pageId: {
1122
+ page_id: {
1121
1123
  type: 'number',
1122
1124
  description: 'Page ID',
1123
1125
  },
1124
- moduleIdentifier: {
1126
+ module_identifier: {
1125
1127
  type: 'object',
1126
1128
  description: 'Module identifier - use one of: admin_label, path, or type. For Elementor: prefer id or path (dot notation like "0.0.1"). admin_label maps to Navigator label or CSS ID. For Divi: admin_label maps to Admin Label field.',
1127
1129
  properties: {
@@ -1161,13 +1163,13 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1161
1163
  },
1162
1164
  },
1163
1165
  },
1164
- editTarget: {
1166
+ edit_target: {
1165
1167
  type: 'string',
1166
1168
  description: 'When editing an original, choose ask, live, or duplicate. Defaults to ask when direct editing is enabled.',
1167
1169
  enum: ['ask', 'live', 'duplicate'],
1168
1170
  },
1169
1171
  },
1170
- required: ['builder', 'pageId', 'moduleIdentifier', 'updates'],
1172
+ required: ['builder', 'page_id', 'module_identifier', 'updates'],
1171
1173
  },
1172
1174
  idempotentHint: true,
1173
1175
  },
@@ -1260,7 +1262,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1260
1262
  type: 'string',
1261
1263
  description: 'Builder slug',
1262
1264
  },
1263
- postId: {
1265
+ post_id: {
1264
1266
  type: 'number',
1265
1267
  description: 'Post ID',
1266
1268
  },
@@ -1273,13 +1275,13 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1273
1275
  description: 'Patch operations[]',
1274
1276
  items: { type: 'object' },
1275
1277
  },
1276
- editTarget: {
1278
+ edit_target: {
1277
1279
  type: 'string',
1278
1280
  description: 'When editing an original, choose ask, live, or duplicate. Defaults to ask when direct editing is enabled.',
1279
1281
  enum: ['ask', 'live', 'duplicate'],
1280
1282
  },
1281
1283
  },
1282
- required: ['builder', 'postId', 'operations'],
1284
+ required: ['builder', 'post_id', 'operations'],
1283
1285
  },
1284
1286
  idempotentHint: true,
1285
1287
  },
@@ -1303,14 +1305,28 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1303
1305
  inputSchema: {
1304
1306
  type: 'object',
1305
1307
  properties: {
1306
- siteId: {
1308
+ site_id: {
1307
1309
  type: 'string',
1308
1310
  description: 'Site ID from configuration',
1309
1311
  },
1310
1312
  },
1311
- required: ['siteId'],
1313
+ required: ['site_id'],
1312
1314
  },
1313
1315
  },
1316
+ {
1317
+ name: 'wordpress_diagnose_connection',
1318
+ description: 'Run a connection-fingerprint diagnostic for the active site. Combines the plugin\'s server-side report (route registration, php/wp/plugin versions, edge plugin presence) with outside-in probes from the MCP server (REST root reachability, content-type sanity check on Respira routes, edge-layer headers). Use when a tool returns "html instead of json", an opaque 5xx, or when a connection that worked yesterday silently breaks. Returns a structured object including detected edge layers (Cloudflare, Wordfence, Sucuri) and concrete remediation recommendations.',
1319
+ inputSchema: {
1320
+ type: 'object',
1321
+ properties: {
1322
+ post_id: {
1323
+ type: 'number',
1324
+ description: 'Optional target post ID for a more specific probe.',
1325
+ },
1326
+ },
1327
+ },
1328
+ readOnlyHint: true,
1329
+ },
1314
1330
  // Page Speed Analysis tools
1315
1331
  {
1316
1332
  name: 'wordpress_analyze_performance',
@@ -1318,12 +1334,12 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1318
1334
  inputSchema: {
1319
1335
  type: 'object',
1320
1336
  properties: {
1321
- pageId: {
1337
+ page_id: {
1322
1338
  type: 'number',
1323
1339
  description: 'Page ID to analyze',
1324
1340
  },
1325
1341
  },
1326
- required: ['pageId'],
1342
+ required: ['page_id'],
1327
1343
  },
1328
1344
  readOnlyHint: true,
1329
1345
  },
@@ -1333,12 +1349,12 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1333
1349
  inputSchema: {
1334
1350
  type: 'object',
1335
1351
  properties: {
1336
- pageId: {
1352
+ page_id: {
1337
1353
  type: 'number',
1338
1354
  description: 'Page ID to analyze',
1339
1355
  },
1340
1356
  },
1341
- required: ['pageId'],
1357
+ required: ['page_id'],
1342
1358
  },
1343
1359
  readOnlyHint: true,
1344
1360
  },
@@ -1348,12 +1364,12 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1348
1364
  inputSchema: {
1349
1365
  type: 'object',
1350
1366
  properties: {
1351
- pageId: {
1367
+ page_id: {
1352
1368
  type: 'number',
1353
1369
  description: 'Page ID to analyze',
1354
1370
  },
1355
1371
  },
1356
- required: ['pageId'],
1372
+ required: ['page_id'],
1357
1373
  },
1358
1374
  readOnlyHint: true,
1359
1375
  },
@@ -1364,12 +1380,12 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1364
1380
  inputSchema: {
1365
1381
  type: 'object',
1366
1382
  properties: {
1367
- pageId: {
1383
+ page_id: {
1368
1384
  type: 'number',
1369
1385
  description: 'Page ID to analyze',
1370
1386
  },
1371
1387
  },
1372
- required: ['pageId'],
1388
+ required: ['page_id'],
1373
1389
  },
1374
1390
  readOnlyHint: true,
1375
1391
  },
@@ -1379,12 +1395,12 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1379
1395
  inputSchema: {
1380
1396
  type: 'object',
1381
1397
  properties: {
1382
- pageId: {
1398
+ page_id: {
1383
1399
  type: 'number',
1384
1400
  description: 'Page ID to analyze',
1385
1401
  },
1386
1402
  },
1387
- required: ['pageId'],
1403
+ required: ['page_id'],
1388
1404
  },
1389
1405
  readOnlyHint: true,
1390
1406
  },
@@ -1394,12 +1410,12 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1394
1410
  inputSchema: {
1395
1411
  type: 'object',
1396
1412
  properties: {
1397
- pageId: {
1413
+ page_id: {
1398
1414
  type: 'number',
1399
1415
  description: 'Page ID to analyze',
1400
1416
  },
1401
1417
  },
1402
- required: ['pageId'],
1418
+ required: ['page_id'],
1403
1419
  },
1404
1420
  readOnlyHint: true,
1405
1421
  },
@@ -1409,12 +1425,12 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1409
1425
  inputSchema: {
1410
1426
  type: 'object',
1411
1427
  properties: {
1412
- postId: {
1428
+ post_id: {
1413
1429
  type: 'number',
1414
1430
  description: 'Post or page ID to analyze',
1415
1431
  },
1416
1432
  },
1417
- required: ['postId'],
1433
+ required: ['post_id'],
1418
1434
  },
1419
1435
  readOnlyHint: true,
1420
1436
  },
@@ -1425,12 +1441,12 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1425
1441
  inputSchema: {
1426
1442
  type: 'object',
1427
1443
  properties: {
1428
- pageId: {
1444
+ page_id: {
1429
1445
  type: 'number',
1430
1446
  description: 'Page ID to analyze',
1431
1447
  },
1432
1448
  },
1433
- required: ['pageId'],
1449
+ required: ['page_id'],
1434
1450
  },
1435
1451
  readOnlyHint: true,
1436
1452
  },
@@ -1440,12 +1456,12 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1440
1456
  inputSchema: {
1441
1457
  type: 'object',
1442
1458
  properties: {
1443
- pageId: {
1459
+ page_id: {
1444
1460
  type: 'number',
1445
1461
  description: 'Page ID to analyze',
1446
1462
  },
1447
1463
  },
1448
- required: ['pageId'],
1464
+ required: ['page_id'],
1449
1465
  },
1450
1466
  readOnlyHint: true,
1451
1467
  },
@@ -1515,7 +1531,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1515
1531
  inputSchema: {
1516
1532
  type: 'object',
1517
1533
  properties: {
1518
- slugOrUrl: {
1534
+ slug_or_url: {
1519
1535
  type: 'string',
1520
1536
  description: 'Plugin slug (from WordPress.org) or ZIP file URL',
1521
1537
  },
@@ -1525,7 +1541,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1525
1541
  enum: ['wordpress.org', 'url'],
1526
1542
  },
1527
1543
  },
1528
- required: ['slugOrUrl'],
1544
+ required: ['slug_or_url'],
1529
1545
  },
1530
1546
  },
1531
1547
  {
@@ -1601,7 +1617,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1601
1617
  type: 'number',
1602
1618
  description: 'Page number',
1603
1619
  },
1604
- perPage: {
1620
+ per_page: {
1605
1621
  type: 'number',
1606
1622
  description: 'Items per page',
1607
1623
  },
@@ -1719,7 +1735,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1719
1735
  type: 'number',
1720
1736
  description: 'Page number',
1721
1737
  },
1722
- perPage: {
1738
+ per_page: {
1723
1739
  type: 'number',
1724
1740
  description: 'Items per page',
1725
1741
  },
@@ -1857,7 +1873,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1857
1873
  type: 'number',
1858
1874
  description: 'Page number',
1859
1875
  },
1860
- perPage: {
1876
+ per_page: {
1861
1877
  type: 'number',
1862
1878
  description: 'Items per page',
1863
1879
  },
@@ -2016,7 +2032,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
2016
2032
  type: 'number',
2017
2033
  description: 'Page number',
2018
2034
  },
2019
- perPage: {
2035
+ per_page: {
2020
2036
  type: 'number',
2021
2037
  description: 'Items per page',
2022
2038
  },
@@ -2117,11 +2133,11 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
2117
2133
  type: 'object',
2118
2134
  description: 'Generic map of taxonomy slug to array of term ids/slugs for custom taxonomies (e.g. product_cat). Example: { "product_cat": [12, "featured"] }.',
2119
2135
  },
2120
- featuredMedia: {
2136
+ featured_media: {
2121
2137
  type: 'number',
2122
2138
  description: 'Attachment ID to set as the featured image.',
2123
2139
  },
2124
- createMissingTerms: {
2140
+ create_missing_terms: {
2125
2141
  type: 'boolean',
2126
2142
  description: 'When true, auto-create taxonomy terms that do not exist by name. Default false (strict: unknown slug returns 400 and the post is rolled back).',
2127
2143
  },
@@ -2194,15 +2210,15 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
2194
2210
  type: 'object',
2195
2211
  description: 'Generic map of taxonomy slug to array of term ids/slugs for custom taxonomies.',
2196
2212
  },
2197
- featuredMedia: {
2213
+ featured_media: {
2198
2214
  type: 'number',
2199
2215
  description: 'Attachment ID to set as the featured image. Pass 0 to clear.',
2200
2216
  },
2201
- createMissingTerms: {
2217
+ create_missing_terms: {
2202
2218
  type: 'boolean',
2203
2219
  description: 'When true, auto-create taxonomy terms that do not exist by name. Default false (strict).',
2204
2220
  },
2205
- appendTerms: {
2221
+ append_terms: {
2206
2222
  type: 'boolean',
2207
2223
  description: 'When true, append to existing taxonomy terms instead of replacing. Default false (replace).',
2208
2224
  },
@@ -2210,11 +2226,11 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
2210
2226
  type: 'object',
2211
2227
  description: 'Post meta data (e.g., { "_et_pb_custom_css": "/* CSS */" })',
2212
2228
  },
2213
- customCss: {
2229
+ custom_css: {
2214
2230
  type: 'string',
2215
2231
  description: 'Custom CSS for page builders (Divi/Elementor). Automatically saved to the appropriate meta field based on detected builder.',
2216
2232
  },
2217
- editTarget: {
2233
+ edit_target: {
2218
2234
  type: 'string',
2219
2235
  description: 'Choose ask, live, or duplicate when the target is an original item.',
2220
2236
  enum: ['ask', 'live', 'duplicate'],
@@ -3057,12 +3073,79 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3057
3073
  },
3058
3074
  ];
3059
3075
  }
3076
+ /**
3077
+ * camelCase → snake_case rename map (mcp-v6.11.1).
3078
+ *
3079
+ * Schemas advertise snake_case only. Handlers can read either form for one
3080
+ * release; camelCase emits a deprecation warning to stderr and will be
3081
+ * removed in mcp-v6.13.
3082
+ *
3083
+ * Mirrors both directions so wordpress-client.ts (which still reads
3084
+ * camelCase fields like data.customCss / data.featuredMedia / args.edit_target
3085
+ * before mapping them to plugin-side snake_case) keeps working unchanged.
3086
+ * The wordpress-client surface is purely internal — its rename can ride
3087
+ * along in a follow-up release without breaking callers.
3088
+ */
3089
+ static CAMEL_TO_SNAKE_PARAMS = {
3090
+ pageId: 'page_id',
3091
+ postId: 'post_id',
3092
+ perPage: 'per_page',
3093
+ editTarget: 'edit_target',
3094
+ originalId: 'original_id',
3095
+ customCss: 'custom_css',
3096
+ skipSecurityCheck: 'skip_security_check',
3097
+ featuredMedia: 'featured_media',
3098
+ createMissingTerms: 'create_missing_terms',
3099
+ appendTerms: 'append_terms',
3100
+ mimeType: 'mime_type',
3101
+ moduleIdentifier: 'module_identifier',
3102
+ diviVersion: 'divi_version',
3103
+ slugOrUrl: 'slug_or_url',
3104
+ };
3105
+ /**
3106
+ * Normalize tool args so handlers see both snake_case (new, advertised in
3107
+ * the schema) and camelCase (legacy, kept working for one release with a
3108
+ * deprecation warning). Mutates the args object in place and returns it.
3109
+ */
3110
+ normalizeArgsForBackCompat(toolName, args) {
3111
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
3112
+ return args;
3113
+ }
3114
+ for (const [camel, snake] of Object.entries(RespiraWordPressServer.CAMEL_TO_SNAKE_PARAMS)) {
3115
+ const hasCamel = Object.prototype.hasOwnProperty.call(args, camel);
3116
+ const hasSnake = Object.prototype.hasOwnProperty.call(args, snake);
3117
+ if (hasCamel && !hasSnake) {
3118
+ // Caller is on the legacy camelCase path. Mirror to snake_case so
3119
+ // schema-aware downstream code can read it; warn once.
3120
+ args[snake] = args[camel];
3121
+ try {
3122
+ process.stderr.write(`[respira-mcp] deprecated: '${camel}' is renamed to '${snake}', will be removed in mcp-v6.13 (tool: ${toolName})\n`);
3123
+ }
3124
+ catch {
3125
+ // ignore stderr write failures.
3126
+ }
3127
+ }
3128
+ else if (!hasCamel && hasSnake) {
3129
+ // Caller is on the new snake_case path. Mirror to camelCase so the
3130
+ // wordpress-client.ts layer (which still reads camelCase) keeps
3131
+ // working without further surface changes this release.
3132
+ args[camel] = args[snake];
3133
+ }
3134
+ // If both are present, prefer snake_case as source of truth and leave
3135
+ // both in place. If neither, no-op.
3136
+ }
3137
+ return args;
3138
+ }
3060
3139
  async handleToolCall(name, args) {
3061
3140
  if (!this.currentSite) {
3062
3141
  throw new Error('No WordPress site configured');
3063
3142
  }
3064
3143
  // Normalize respira_* ↔ wordpress_* names.
3065
3144
  const { canonical, deprecated } = this.normalizeToolName(name);
3145
+ // mcp-v6.11.1: snake_case sweep across tool catalog. Schemas advertise
3146
+ // snake_case; legacy camelCase still accepted with a deprecation warning
3147
+ // for one release.
3148
+ args = this.normalizeArgsForBackCompat(canonical, args);
3066
3149
  const result = await this.dispatchToolCall(canonical, args);
3067
3150
  if (deprecated && result && typeof result === 'object' && !Array.isArray(result)) {
3068
3151
  return {
@@ -3104,7 +3187,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3104
3187
  return await this.currentSite.getPage(args.id, args.include);
3105
3188
  case 'wordpress_create_page_duplicate':
3106
3189
  return {
3107
- ...(await this.currentSite.duplicatePage(args.originalId, args.suffix, args.include)),
3190
+ ...(await this.currentSite.duplicatePage(args.original_id, args.suffix, args.include)),
3108
3191
  respira_approvals_url: this.currentSite.getApprovalsUrl(),
3109
3192
  };
3110
3193
  case 'wordpress_update_page': {
@@ -3137,7 +3220,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3137
3220
  return await this.currentSite.getPost(args.id, args.include);
3138
3221
  case 'wordpress_create_post_duplicate':
3139
3222
  return {
3140
- ...(await this.currentSite.duplicatePost(args.originalId, args.suffix, args.include)),
3223
+ ...(await this.currentSite.duplicatePost(args.original_id, args.suffix, args.include)),
3141
3224
  respira_approvals_url: this.currentSite.getApprovalsUrl(),
3142
3225
  };
3143
3226
  case 'wordpress_update_post': {
@@ -3167,30 +3250,30 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3167
3250
  case 'wordpress_list_media':
3168
3251
  return await this.currentSite.listMedia(args);
3169
3252
  case 'wordpress_upload_media':
3170
- return await this.currentSite.uploadMedia(args.file, args.filename, args.mimeType, args.title, args.alt, args.caption);
3253
+ return await this.currentSite.uploadMedia(args.file, args.filename, args.mime_type, args.title, args.alt, args.caption);
3171
3254
  case 'wordpress_extract_builder_content':
3172
- return await this.currentSite.extractBuilderContent(args.builder, args.pageId);
3255
+ return await this.currentSite.extractBuilderContent(args.builder, args.page_id);
3173
3256
  case 'wordpress_find_builder_targets':
3174
- return await this.currentSite.findBuilderTargets(args.builder, args.pageId, args.query, args.limit);
3257
+ return await this.currentSite.findBuilderTargets(args.builder, args.page_id, args.query, args.limit);
3175
3258
  case 'wordpress_inject_builder_content':
3176
3259
  return {
3177
- ...(await this.currentSite.injectBuilderContent(args.builder, args.pageId, args.content, args.diviVersion, args.editTarget, args.mode)),
3260
+ ...(await this.currentSite.injectBuilderContent(args.builder, args.page_id, args.content, args.divi_version, args.edit_target, args.mode)),
3178
3261
  respira_approvals_url: this.currentSite.getApprovalsUrl(),
3179
3262
  };
3180
3263
  case 'wordpress_update_module':
3181
3264
  return {
3182
- ...(await this.currentSite.updateModule(args.builder, args.pageId, args.moduleIdentifier, args.updates, args.editTarget)),
3265
+ ...(await this.currentSite.updateModule(args.builder, args.page_id, args.module_identifier, args.updates, args.edit_target)),
3183
3266
  respira_approvals_url: this.currentSite.getApprovalsUrl(),
3184
3267
  };
3185
3268
  case 'wordpress_validate_security':
3186
3269
  return await this.currentSite.validateSecurity(args.content);
3187
3270
  case 'wordpress_switch_site': {
3188
- const newSite = this.sites.get(args.siteId);
3271
+ const newSite = this.sites.get(args.site_id);
3189
3272
  if (!newSite) {
3190
- throw new Error(`Site with ID "${args.siteId}" not found in configuration`);
3273
+ throw new Error(`Site with ID "${args.site_id}" not found in configuration`);
3191
3274
  }
3192
3275
  if (!this.isSiteAllowed(newSite)) {
3193
- throw new Error(`Site "${args.siteId}" is not in this MCP configuration group.`);
3276
+ throw new Error(`Site "${args.site_id}" is not in this MCP configuration group.`);
3194
3277
  }
3195
3278
  this.currentSite = newSite;
3196
3279
  this.cachedFilterContext = null; // Invalidate tool filter cache on site switch.
@@ -3200,27 +3283,29 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3200
3283
  site: this.getSiteSummary(newSite),
3201
3284
  };
3202
3285
  }
3286
+ case 'wordpress_diagnose_connection':
3287
+ return await this.currentSite.diagnoseConnection({ post_id: args.post_id });
3203
3288
  // Page Speed Analysis
3204
3289
  case 'wordpress_analyze_performance':
3205
- return await this.currentSite.analyzePerformance(args.pageId);
3290
+ return await this.currentSite.analyzePerformance(args.page_id);
3206
3291
  case 'wordpress_get_core_web_vitals':
3207
- return await this.currentSite.getCoreWebVitals(args.pageId);
3292
+ return await this.currentSite.getCoreWebVitals(args.page_id);
3208
3293
  case 'wordpress_analyze_images':
3209
- return await this.currentSite.analyzeImages(args.pageId);
3294
+ return await this.currentSite.analyzeImages(args.page_id);
3210
3295
  // SEO Analysis
3211
3296
  case 'wordpress_analyze_seo':
3212
- return await this.currentSite.analyzeSEO(args.pageId);
3297
+ return await this.currentSite.analyzeSEO(args.page_id);
3213
3298
  case 'wordpress_check_seo_issues':
3214
- return await this.currentSite.checkSEOIssues(args.pageId);
3299
+ return await this.currentSite.checkSEOIssues(args.page_id);
3215
3300
  case 'wordpress_analyze_readability':
3216
- return await this.currentSite.analyzeReadability(args.pageId);
3301
+ return await this.currentSite.analyzeReadability(args.page_id);
3217
3302
  case 'wordpress_analyze_rankmath':
3218
- return await this.currentSite.analyzeRankMath(args.postId);
3303
+ return await this.currentSite.analyzeRankMath(args.post_id);
3219
3304
  // AEO Analysis
3220
3305
  case 'wordpress_analyze_aeo':
3221
- return await this.currentSite.analyzeAEO(args.pageId);
3306
+ return await this.currentSite.analyzeAEO(args.page_id);
3222
3307
  case 'wordpress_check_structured_data':
3223
- return await this.currentSite.checkStructuredData(args.pageId);
3308
+ return await this.currentSite.checkStructuredData(args.page_id);
3224
3309
  // Accessibility
3225
3310
  case 'wordpress_list_accessibility_scans':
3226
3311
  return await this.currentSite.listAccessibilityScans();
@@ -3234,7 +3319,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3234
3319
  case 'wordpress_list_plugins':
3235
3320
  return await this.currentSite.listPlugins();
3236
3321
  case 'wordpress_install_plugin':
3237
- return await this.currentSite.installPlugin(args.slugOrUrl, args.source || 'wordpress.org');
3322
+ return await this.currentSite.installPlugin(args.slug_or_url, args.source || 'wordpress.org');
3238
3323
  case 'wordpress_activate_plugin':
3239
3324
  return await this.currentSite.activatePlugin(args.slug);
3240
3325
  case 'wordpress_deactivate_plugin':
@@ -3349,7 +3434,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3349
3434
  case 'wordpress_restore_snapshot':
3350
3435
  return await this.currentSite.restoreSnapshot(args.snapshot_uuid);
3351
3436
  case 'wordpress_apply_builder_patch':
3352
- return await this.currentSite.applyBuilderPatch(args.builder, args.postId, args.operations, args.include, args.editTarget);
3437
+ return await this.currentSite.applyBuilderPatch(args.builder, args.post_id, args.operations, args.include, args.edit_target);
3353
3438
  case 'woocommerce_list_products':
3354
3439
  return await this.currentSite.woocommerceListProducts(args);
3355
3440
  case 'woocommerce_get_product':
@@ -3566,14 +3651,22 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3566
3651
  },
3567
3652
  {
3568
3653
  name: 'wordpress_remove_element',
3569
- description: 'Remove an element from a page.',
3654
+ description: 'Remove an element from a page. Use the same identifier_type/identifier_value pattern as find_element and update_element — first locate the element, then pass the matching identifier to remove it.',
3570
3655
  inputSchema: {
3571
3656
  type: 'object',
3572
3657
  properties: {
3573
3658
  post_id: { type: 'number', description: 'Page/post ID' },
3574
- element_id: { type: 'string', description: 'Element ID to remove' },
3659
+ identifier_type: {
3660
+ type: 'string',
3661
+ enum: ['id', 'css_class', 'text', 'widget_type', 'global_id'],
3662
+ description: 'How to locate the element: "id" (element ID), "css_class" (CSS class name), "text" (visible text content), "widget_type" (widget/module type), or "global_id" (cross-page global element ID).',
3663
+ },
3664
+ identifier_value: {
3665
+ type: 'string',
3666
+ description: 'Value matching the chosen identifier_type (e.g. the element ID, the class name, the text to match).',
3667
+ },
3575
3668
  },
3576
- required: ['post_id', 'element_id'],
3669
+ required: ['post_id', 'identifier_type', 'identifier_value'],
3577
3670
  },
3578
3671
  destructiveHint: true,
3579
3672
  },