@respira/wordpress-mcp-server 4.0.8 → 5.2.0

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
@@ -14,7 +14,44 @@ export class RespiraWordPressServer {
14
14
  sites = new Map();
15
15
  defaultSiteId = null;
16
16
  versionChecker;
17
- static MCP_SERVER_VERSION = '4.0.0';
17
+ static MCP_SERVER_VERSION = '5.2.0';
18
+ /**
19
+ * Normalize a tool name: respira_* → wordpress_* for switch dispatch.
20
+ * Tracks whether the deprecated wordpress_* name was used.
21
+ */
22
+ normalizeToolName(name) {
23
+ if (name.startsWith('respira_')) {
24
+ return { canonical: 'wordpress_' + name.slice('respira_'.length), deprecated: false };
25
+ }
26
+ if (name.startsWith('wordpress_')) {
27
+ return { canonical: name, deprecated: true };
28
+ }
29
+ // woocommerce_* and other names pass through unchanged.
30
+ return { canonical: name, deprecated: false };
31
+ }
32
+ /**
33
+ * Generate respira_* aliases for all wordpress_* tools.
34
+ * The wordpress_* versions get a deprecation prefix in their description.
35
+ */
36
+ generateDualTools(tools) {
37
+ const result = [];
38
+ for (const tool of tools) {
39
+ if (tool.name.startsWith('wordpress_')) {
40
+ const respiraName = 'respira_' + tool.name.slice('wordpress_'.length);
41
+ // Add the respira_* version (preferred).
42
+ result.push({ ...tool, name: respiraName });
43
+ // Keep the wordpress_* version with deprecation notice.
44
+ result.push({
45
+ ...tool,
46
+ description: `[Deprecated — use ${respiraName} instead] ${tool.description}`,
47
+ });
48
+ }
49
+ else {
50
+ result.push(tool);
51
+ }
52
+ }
53
+ return result;
54
+ }
18
55
  constructor(siteConfigs) {
19
56
  this.server = new Server({
20
57
  name: 'respira-wordpress',
@@ -23,6 +60,181 @@ export class RespiraWordPressServer {
23
60
  capabilities: {
24
61
  tools: {},
25
62
  },
63
+ instructions: `Respira MCP Server v${RespiraWordPressServer.MCP_SERVER_VERSION} — 151 tools for WordPress (130 core + 21 WooCommerce).
64
+
65
+ 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.
66
+
67
+ ## Tool naming
68
+
69
+ 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.
70
+
71
+ ## Step 1: Understand the site
72
+
73
+ Before making changes, check what you're working with:
74
+ - respira_get_site_context — WordPress version, theme, active plugins, site URL
75
+ - respira_get_builder_info — which page builder is active, its version, available modules, and support level
76
+
77
+ ## Step 2: Find content
78
+
79
+ - respira_find_element — search by text content, CSS class, widget type, or element ID (BEST for targeted edits)
80
+ - respira_find_builder_targets — list all editable targets on a page (lighter than full extract)
81
+ - respira_extract_builder_content — get the full page structure as JSON (use when you need the complete layout)
82
+ - respira_list_pages / respira_list_posts — find pages/posts by title or search term
83
+ - respira_read_page / respira_read_post — get full content including meta and builder info
84
+
85
+ ## Step 3: Make changes
86
+
87
+ For small edits (text, dates, colors, settings):
88
+ - respira_update_element — update a single element's settings or content
89
+ - respira_batch_update — update multiple elements on one page atomically (extracts once, injects once)
90
+
91
+ For structural changes (move, copy, delete):
92
+ - respira_move_element / respira_reorder_elements — rearrange elements within or across containers
93
+ - respira_duplicate_element — clone an element (inserted after the original)
94
+ - respira_remove_element — delete an element
95
+
96
+ For full page changes:
97
+ - respira_inject_builder_content — replace entire page content (extract first, modify, inject)
98
+ - respira_build_page — create a complete new page from a declarative builder-native structure
99
+ - respira_convert_html_to_builder — convert HTML/CSS prototypes into native builder content with fidelity scoring
100
+ - respira_bulk_pages_operation — apply changes across up to 100 pages (strip_inline_styles, find_and_replace, custom)
101
+
102
+ For adding new content:
103
+ - 27 widget shortcuts: respira_add_heading, respira_add_text, respira_add_button, respira_add_image, respira_add_video, respira_add_section, respira_add_divider, respira_add_spacer, respira_add_icon, respira_add_icon_list, respira_add_social_icons, respira_add_form, respira_add_map, respira_add_counter, respira_add_progress_bar, respira_add_testimonial, respira_add_tabs, respira_add_accordion, respira_add_toggle, respira_add_alert, respira_add_html, respira_add_menu, respira_add_sidebar, respira_add_search, respira_add_gallery, respira_add_slider, respira_add_pricing_table
104
+
105
+ ## Common tasks — the right way
106
+
107
+ - "Change a date/text/heading on a page" → respira_find_element (search by the current text) + respira_update_element
108
+ - "Update the footer" → respira_find_element (search by text or CSS class like "footer" or "copyright") + respira_update_element. Footer content lives in the page builder, NOT in theme PHP files.
109
+ - "Change the copyright year" → respira_find_element with the old year as search text + respira_update_element with the new year
110
+ - "Update site title or tagline" → respira_update_option (option_name: "blogname" or "blogdescription")
111
+ - "Add a new section to a page" → respira_add_section or use widget shortcuts
112
+ - "Redesign/rebuild a page" → respira_extract_builder_content → modify the JSON → respira_inject_builder_content
113
+ - "Create a page from HTML" → respira_convert_html_to_builder
114
+ - "Create a page from scratch" → respira_build_page with builder-native structure
115
+ - "Update a navigation menu" → respira_list_menus → respira_list_menu_items → respira_update_menu_item or respira_create_menu_item
116
+ - "Find which page contains specific text" → respira_list_pages with search parameter, then respira_find_element on matching pages
117
+ - "Change an image" → respira_find_element (by type "image" or current alt text) + respira_update_element with new image URL
118
+ - "Add stock photos" → respira_search_stock_images + respira_sideload_image to download into Media Library
119
+ - "Check SEO" → respira_analyze_seo or respira_analyze_rankmath (for RankMath sites)
120
+ - "Optimize images" → respira_analyze_images for recommendations
121
+ - "Undo a change" → respira_list_snapshots + respira_restore_snapshot
122
+ - "Compare before/after" → respira_diff_snapshots
123
+
124
+ ## Page builders — how each one works
125
+
126
+ Use respira_get_builder_info first to detect which builder is active. Then use the appropriate content format:
127
+
128
+ ### Elementor (Full Intelligence)
129
+ - Content format: Array of sections containing columns containing widgets
130
+ - Each widget has a widgetType (e.g. "heading", "text-editor", "image", "button") and settings object
131
+ - Dynamic schemas: Respira reads Elementor's control registry at runtime, so it knows the exact settings, types, and valid values for every widget
132
+ - Settings validator catches typos: "Unknown setting 'titel'. Did you mean 'title'?"
133
+ - Responsive: settings support _mobile and _tablet suffixes (e.g. "align" for desktop, "align_mobile" for mobile)
134
+ - respira_find_element works with Elementor widget IDs, types, CSS classes, and text content
135
+ - respira_inject_builder_content expects Elementor-native JSON structure
136
+ - HTML-to-Elementor conversion creates native widgets with mapped CSS styles, responsive breakpoints, and optional global color/font creation
137
+
138
+ ### Divi 5 (Full Intelligence)
139
+ - Content format: Block comments wrapping JSON module definitions inside <!-- wp:divi/placeholder -->
140
+ - Every module needs "builderVersion": "5.0.1"
141
+ - Hierarchy is strict: Section → Row → Column → Module (never skip levels)
142
+ - Self-closing modules end with " /-->" (space + slash), container modules need matching close tags
143
+ - Flex layout: flexColumnStructure count must match actual column children count
144
+ - Flex fractions: 24_24=100%, 12_24=50%, 8_24=33.3%, 6_24=25%
145
+ - Design tokens: color variables use $variable({"type":"color","value":{"name":"gcid-ID","settings":{}}})$ syntax — always include "settings":{}
146
+ - Heading title is plain text only (no HTML tags); rich text uses \\u003c/\\u003e encoding
147
+ - Button uses linkUrl/linkTarget (NOT Divi 4's url/newWindow)
148
+ - Blurb title must be {"text": "..."} object format, not a plain string
149
+ - 40+ module definitions and 7 page patterns available
150
+ - 13-point payload validation with 16 actionable error messages
151
+
152
+ ### Divi 4 Legacy (Smart Defaults)
153
+ - Content format: Shortcode-based — [et_pb_section][et_pb_row][et_pb_column][et_pb_text]content[/et_pb_text][/et_pb_column][/et_pb_row][/et_pb_section]
154
+ - Hierarchy: Section → Row → Column → Module (same as Divi 5, but shortcodes instead of block comments)
155
+ - 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.
156
+ - Settings are shortcode attributes: [et_pb_text text_font="Roboto" text_text_color="#333333"]
157
+ - Button uses url/url_new_window attributes (different from Divi 5's linkUrl/linkTarget)
158
+ - IMPORTANT: When injecting content, set diviVersion to "4" in respira_inject_builder_content
159
+ - Respira auto-detects whether the site runs Divi 4 or Divi 5 — check respira_get_builder_info for the version
160
+ - Element-level operations work via shortcode tree parsing
161
+
162
+ ### Gutenberg / Block Editor (Full Intelligence)
163
+ - Content format: Array of block objects with blockName (or type), attrs, and innerBlocks
164
+ - Accepts both simplified format ({"type": "core/heading", "attrs": {"content": "Hello"}}) and native format ({"blockName": "core/heading", ...})
165
+ - Wrapping in {"blocks": [...]} is accepted and auto-unwrapped
166
+ - Common blocks: core/heading, core/paragraph, core/image, core/button, core/columns, core/group, core/cover, core/list
167
+ - Style via attrs.style object or className for theme presets
168
+
169
+ ### Beaver Builder (Smart Defaults)
170
+ - Content format: Rows containing column-groups containing columns containing modules
171
+ - Module types: heading, rich-text, photo, button, video, separator, html
172
+ - Settings use BB-native keys (e.g. "heading" for heading text, "photo_src" for image URL)
173
+ - Builder identifier: use "beaver" or "beaver-builder" in respira_inject_builder_content
174
+ - HTML-to-Beaver conversion maps to flat node structure with type mapping
175
+
176
+ ### Bricks (Smart Defaults)
177
+ - Content format: Flat array of elements with parent/children references
178
+ - Each element has a "name" (e.g. "section", "container", "heading", "text-basic", "image", "button")
179
+ - Settings in element.settings object
180
+ - CSS regeneration happens automatically on inject and approval merge
181
+ - Cache invalidation on approval merge is automatic
182
+
183
+ ### Breakdance (Smart Defaults)
184
+ - Tree-based content structure
185
+ - Uses element-level operations via the generic tree utility
186
+ - respira_find_element and respira_update_element work via default tree traversal
187
+
188
+ ### Oxygen (Smart Defaults)
189
+ - Content stored in ct_builder_shortcodes and/or ct_builder_json meta
190
+ - Templates use ct_template post type
191
+ - respira_find_element works via tree traversal on the parsed structure
192
+
193
+ ### WPBakery (Smart Defaults)
194
+ - Shortcode-based content format: [vc_row][vc_column][vc_column_text]content[/vc_column_text][/vc_column][/vc_row]
195
+ - Tree-based operations work via parsed shortcode tree
196
+
197
+ ### Brizy, Thrive, Visual Composer (Basic Support)
198
+ - Basic read/write via extract/inject
199
+ - Element-level operations use best-effort tree traversal
200
+ - Some operations may return "not supported" for builder-specific features
201
+
202
+ ## Site management tools
203
+
204
+ - respira_list_pages / respira_read_page — page CRUD with full builder + meta support
205
+ - respira_list_posts / respira_read_post — post CRUD
206
+ - respira_list_media / respira_upload_media / respira_update_media — media library management
207
+ - respira_search_stock_images / respira_sideload_image — find and import CC-licensed stock photos with auto-attribution
208
+ - respira_list_menus / respira_create_menu / respira_update_menu_item — navigation menus
209
+ - respira_list_options / respira_get_option / respira_update_option — WordPress settings (site title, tagline, timezone, etc.)
210
+ - respira_list_users / respira_create_user / respira_update_user — user management
211
+ - respira_list_comments / respira_create_comment — comment management
212
+ - respira_list_taxonomies / respira_list_terms / respira_create_term — categories, tags, custom taxonomies
213
+ - respira_list_custom_posts — custom post types (WooCommerce products, portfolio items, etc.)
214
+ - respira_list_plugins / respira_install_plugin / respira_activate_plugin — plugin management (experimental, must be enabled in settings)
215
+
216
+ ## Analysis tools
217
+
218
+ - respira_analyze_seo — comprehensive SEO audit (meta tags, headings, images, links, schema)
219
+ - respira_analyze_rankmath — RankMath-specific score breakdown with ready-to-apply fixes
220
+ - respira_analyze_aeo — AI Engine Optimization (Perplexity, ChatGPT, AI search)
221
+ - respira_check_seo_issues — quick SEO issue check
222
+ - respira_check_structured_data — JSON-LD and schema validation
223
+ - respira_analyze_performance — page performance metrics
224
+ - respira_get_core_web_vitals — LCP, FID, CLS metrics
225
+ - respira_analyze_images — image optimization opportunities
226
+ - respira_analyze_readability — Flesch score, sentence/paragraph analysis
227
+
228
+ ## Safety features
229
+
230
+ - Snapshots: respira_list_snapshots, respira_get_snapshot, respira_diff_snapshots, respira_restore_snapshot — every edit creates a rollback point
231
+ - Duplicate workflow: respira_create_page_duplicate creates a safe copy; edits go to the duplicate; user approves in wp-admin to go live
232
+ - Direct edit mode: when enabled in settings, writes go straight to the original (skip duplicate workflow)
233
+ - respira_validate_security — check content for XSS and security issues before saving
234
+
235
+ ## WooCommerce (when addon installed)
236
+
237
+ 21 tools for product management, categories, tags, orders, stock, and sales reports. All prefixed woocommerce_*.`,
26
238
  });
27
239
  this.versionChecker = new RespiraVersionChecker('@respira/wordpress-mcp-server', RespiraWordPressServer.MCP_SERVER_VERSION);
28
240
  // Initialize WordPress clients for each site
@@ -255,7 +467,7 @@ export class RespiraWordPressServer {
255
467
  },
256
468
  {
257
469
  name: 'wordpress_get_builder_info',
258
- description: 'Get information about the active page builder including available modules/widgets.',
470
+ description: 'Detect which page builder is active on the site and get its version, support level (Full Intelligence / Smart Defaults / Basic), available modules/widgets, and capabilities. Call this first before working with builder content to understand what format to use.',
259
471
  inputSchema: {
260
472
  type: 'object',
261
473
  properties: {},
@@ -596,13 +808,13 @@ export class RespiraWordPressServer {
596
808
  },
597
809
  {
598
810
  name: 'wordpress_extract_builder_content',
599
- description: 'Extract structured content from a page builder (Divi, Elementor, etc.).',
811
+ description: 'Extract the full structured content from a page as builder-native JSON. Use this to see the complete page layout with all sections, columns, and widgets. For targeted searches, prefer find_element (faster). For a lightweight overview, prefer find_builder_targets. Returns the raw builder data structure that can be modified and passed back to inject_builder_content.',
600
812
  inputSchema: {
601
813
  type: 'object',
602
814
  properties: {
603
815
  builder: {
604
816
  type: 'string',
605
- description: 'Builder name (gutenberg, divi, elementor, etc.)',
817
+ 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.',
606
818
  },
607
819
  pageId: {
608
820
  type: 'number',
@@ -615,13 +827,13 @@ export class RespiraWordPressServer {
615
827
  },
616
828
  {
617
829
  name: 'wordpress_find_builder_targets',
618
- description: 'Find likely builder modules or blocks by text, label, or type without returning the full builder payload.',
830
+ description: 'List all editable builder modules/blocks on a page with their types, labels, and text content — without returning the full builder JSON. Use this for a quick overview of what is on a page. For finding a specific element by text or class, use find_element instead.',
619
831
  inputSchema: {
620
832
  type: 'object',
621
833
  properties: {
622
834
  builder: {
623
835
  type: 'string',
624
- description: 'Builder name (gutenberg, divi, elementor, etc.)',
836
+ 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.',
625
837
  },
626
838
  pageId: {
627
839
  type: 'number',
@@ -642,13 +854,13 @@ export class RespiraWordPressServer {
642
854
  },
643
855
  {
644
856
  name: 'wordpress_inject_builder_content',
645
- description: 'Inject structured content into a page builder. For Divi, diviVersion confirmation is required on every inject call ("4" or "5").',
857
+ 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").',
646
858
  inputSchema: {
647
859
  type: 'object',
648
860
  properties: {
649
861
  builder: {
650
862
  type: 'string',
651
- description: 'Builder name',
863
+ description: 'Builder identifier. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery.',
652
864
  },
653
865
  pageId: {
654
866
  type: 'number',
@@ -656,7 +868,12 @@ export class RespiraWordPressServer {
656
868
  },
657
869
  content: {
658
870
  type: 'object',
659
- description: 'Structured content to inject',
871
+ description: 'Structured content to inject in simplified format. In "replace" mode (default), this REPLACES all existing content. In "append" mode, this is added after existing content. For Gutenberg: array of blocks [{type: "core/heading", attrs: {level: 2}, content: "Hello"}, {type: "core/paragraph", attrs: {}, content: "Text here"}]. For Beaver Builder: {rows: [{cols: [{modules: [{type: "rich-text", settings: {text: "..."}}]}]}]}. For Elementor: array of widget objects. For Divi: Divi shortcode or block structure. Use extract_builder_content to see the format for any builder.',
872
+ },
873
+ mode: {
874
+ type: 'string',
875
+ description: 'replace (default): OVERWRITES all existing page content. append: adds new content after existing elements, preserving the current layout.',
876
+ enum: ['replace', 'append'],
660
877
  },
661
878
  editTarget: {
662
879
  type: 'string',
@@ -681,7 +898,7 @@ export class RespiraWordPressServer {
681
898
  properties: {
682
899
  builder: {
683
900
  type: 'string',
684
- description: 'Builder name (e.g., divi, elementor)',
901
+ description: 'Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery.',
685
902
  },
686
903
  pageId: {
687
904
  type: 'number',
@@ -837,6 +1054,7 @@ export class RespiraWordPressServer {
837
1054
  operations: {
838
1055
  type: 'array',
839
1056
  description: 'Patch operations[]',
1057
+ items: { type: 'object' },
840
1058
  },
841
1059
  editTarget: {
842
1060
  type: 'string',
@@ -2040,10 +2258,13 @@ export class RespiraWordPressServer {
2040
2258
  destructiveHint: true,
2041
2259
  },
2042
2260
  ];
2261
+ // --- v5.2.0 Elemental: new tools ---
2262
+ tools.push(...this.getElementalTools());
2043
2263
  if (await this.isWooCommerceAddonAvailable()) {
2044
2264
  tools.push(...this.getWooCommerceTools());
2045
2265
  }
2046
- return tools;
2266
+ // Generate respira_* aliases for all wordpress_* tools.
2267
+ return this.generateDualTools(tools);
2047
2268
  }
2048
2269
  async isWooCommerceAddonAvailable() {
2049
2270
  if (!this.currentSite) {
@@ -2128,7 +2349,7 @@ export class RespiraWordPressServer {
2128
2349
  },
2129
2350
  categories: {
2130
2351
  type: 'array',
2131
- items: {},
2352
+ items: { type: 'object' },
2132
2353
  description: 'Alternative category payload (IDs, names, slugs, or objects with id/name/slug)',
2133
2354
  },
2134
2355
  tag_ids: {
@@ -2138,7 +2359,7 @@ export class RespiraWordPressServer {
2138
2359
  },
2139
2360
  tags: {
2140
2361
  type: 'array',
2141
- items: {},
2362
+ items: { type: 'object' },
2142
2363
  description: 'Alternative tag payload (IDs, names, slugs, or objects with id/name/slug)',
2143
2364
  },
2144
2365
  },
@@ -2369,6 +2590,22 @@ export class RespiraWordPressServer {
2369
2590
  ];
2370
2591
  }
2371
2592
  async handleToolCall(name, args) {
2593
+ if (!this.currentSite) {
2594
+ throw new Error('No WordPress site configured');
2595
+ }
2596
+ // Normalize respira_* ↔ wordpress_* names.
2597
+ const { canonical, deprecated } = this.normalizeToolName(name);
2598
+ const result = await this.dispatchToolCall(canonical, args);
2599
+ if (deprecated && result && typeof result === 'object' && !Array.isArray(result)) {
2600
+ return {
2601
+ ...result,
2602
+ _deprecation_notice: `Tool name "${name}" is deprecated. Use "respira_${name.slice('wordpress_'.length)}" instead. ` +
2603
+ 'wordpress_* names will be removed in v6.0.',
2604
+ };
2605
+ }
2606
+ return result;
2607
+ }
2608
+ async dispatchToolCall(name, args) {
2372
2609
  if (!this.currentSite) {
2373
2610
  throw new Error('No WordPress site configured');
2374
2611
  }
@@ -2466,7 +2703,7 @@ export class RespiraWordPressServer {
2466
2703
  return await this.currentSite.findBuilderTargets(args.builder, args.pageId, args.query, args.limit);
2467
2704
  case 'wordpress_inject_builder_content':
2468
2705
  return {
2469
- ...(await this.currentSite.injectBuilderContent(args.builder, args.pageId, args.content, args.diviVersion, args.editTarget)),
2706
+ ...(await this.currentSite.injectBuilderContent(args.builder, args.pageId, args.content, args.diviVersion, args.editTarget, args.mode)),
2470
2707
  respira_approvals_url: this.currentSite.getApprovalsUrl(),
2471
2708
  };
2472
2709
  case 'wordpress_update_module':
@@ -2677,10 +2914,633 @@ export class RespiraWordPressServer {
2677
2914
  }
2678
2915
  case 'woocommerce_sales_report':
2679
2916
  return await this.currentSite.woocommerceSalesReport(args);
2680
- default:
2917
+ // --- v5.2.0 Elemental tools ---
2918
+ case 'wordpress_find_element': {
2919
+ const { post_id, ...rest } = args;
2920
+ return await this.currentSite.callRestV2('POST', `/builder/elements/find/${post_id}`, rest);
2921
+ }
2922
+ case 'wordpress_update_element': {
2923
+ const { post_id, ...rest } = args;
2924
+ return await this.currentSite.callRestV2('POST', `/builder/elements/update/${post_id}`, rest);
2925
+ }
2926
+ case 'wordpress_move_element': {
2927
+ const { post_id, ...rest } = args;
2928
+ return await this.currentSite.callRestV2('POST', `/builder/elements/move/${post_id}`, rest);
2929
+ }
2930
+ case 'wordpress_duplicate_element': {
2931
+ const { post_id, ...rest } = args;
2932
+ return await this.currentSite.callRestV2('POST', `/builder/elements/duplicate/${post_id}`, rest);
2933
+ }
2934
+ case 'wordpress_remove_element': {
2935
+ const { post_id, ...rest } = args;
2936
+ return await this.currentSite.callRestV2('POST', `/builder/elements/remove/${post_id}`, rest);
2937
+ }
2938
+ case 'wordpress_batch_update': {
2939
+ const { post_id, ...rest } = args;
2940
+ return await this.currentSite.callRestV2('POST', `/builder/elements/batch/${post_id}`, rest);
2941
+ }
2942
+ case 'wordpress_reorder_elements': {
2943
+ const { post_id, ...rest } = args;
2944
+ return await this.currentSite.callRestV2('POST', `/builder/elements/reorder/${post_id}`, rest);
2945
+ }
2946
+ case 'wordpress_build_page':
2947
+ return await this.currentSite.callRestV2('POST', '/builder/build', args);
2948
+ case 'wordpress_convert_html_to_builder':
2949
+ return await this.currentSite.callRestV2('POST', '/builder/convert', args);
2950
+ case 'wordpress_bulk_pages_operation':
2951
+ return await this.currentSite.callRestV2('POST', '/builder/bulk', args);
2952
+ case 'wordpress_search_stock_images':
2953
+ return await this.currentSite.callRestV2('GET', '/stock-images/search', args);
2954
+ case 'wordpress_sideload_image':
2955
+ return await this.currentSite.callRestV2('POST', '/stock-images/sideload', args);
2956
+ case 'wordpress_get_server_compatibility':
2957
+ return await this.currentSite.callRestV2('GET', '/server/compatibility');
2958
+ // Widget shortcuts — single dispatch via registry.
2959
+ default: {
2960
+ // Check if this is a widget shortcut (wordpress_add_*).
2961
+ if (name.startsWith('wordpress_add_')) {
2962
+ const shortcutName = name.slice('wordpress_'.length);
2963
+ const postId = args.post_id;
2964
+ if (!postId) {
2965
+ throw new Error(`Widget shortcut "${shortcutName}" requires a post_id parameter.`);
2966
+ }
2967
+ const { post_id: _pid, ...settings } = args;
2968
+ return await this.currentSite.callRestV2('POST', `/builder/widget/${shortcutName}/${postId}`, settings);
2969
+ }
2681
2970
  throw new Error(`Unknown tool: ${name}`);
2971
+ }
2682
2972
  }
2683
2973
  }
2974
+ /**
2975
+ * v5.2.0 Elemental tool definitions.
2976
+ */
2977
+ getElementalTools() {
2978
+ return [
2979
+ // --- Element Operations ---
2980
+ {
2981
+ name: 'wordpress_find_element',
2982
+ description: 'Find an element in a page by ID, type, CSS class, or text content. This is the PRIMARY tool for locating content to edit — use it instead of searching the database or reading PHP files. Works with all 11 page builders. Use identifier_type "content" to search by visible text (e.g. find a heading containing "2025" to update a year). Returns matching element(s) with their position, settings, and element ID for use with update_element.',
2983
+ inputSchema: {
2984
+ type: 'object',
2985
+ properties: {
2986
+ post_id: { type: 'number', description: 'Page/post ID' },
2987
+ identifier_type: {
2988
+ type: 'string',
2989
+ description: 'How to find the element',
2990
+ enum: ['id', 'type', 'class', 'content'],
2991
+ },
2992
+ identifier_value: { type: 'string', description: 'Value to search for' },
2993
+ match_content: {
2994
+ type: 'string',
2995
+ description: 'Optional text content to narrow results when searching by type/class',
2996
+ },
2997
+ },
2998
+ required: ['post_id', 'identifier_type', 'identifier_value'],
2999
+ },
3000
+ readOnlyHint: true,
3001
+ },
3002
+ {
3003
+ name: 'wordpress_update_element',
3004
+ description: 'Update settings or content on a specific element in a page. This is the PRIMARY tool for making content changes — use it for text edits, style changes, image swaps, link updates, etc. Works with all 11 page builders. First use find_element to locate the element, then pass the same identifier here with the updates object containing the new values.',
3005
+ inputSchema: {
3006
+ type: 'object',
3007
+ properties: {
3008
+ post_id: { type: 'number', description: 'Page/post ID' },
3009
+ identifier_type: {
3010
+ type: 'string',
3011
+ enum: ['id', 'type', 'class', 'content'],
3012
+ },
3013
+ identifier_value: { type: 'string', description: 'Value to match' },
3014
+ updates: {
3015
+ type: 'object',
3016
+ description: 'Key-value pairs of settings to update on the matched element',
3017
+ },
3018
+ },
3019
+ required: ['post_id', 'identifier_type', 'identifier_value', 'updates'],
3020
+ },
3021
+ },
3022
+ {
3023
+ name: 'wordpress_move_element',
3024
+ description: 'Move an element to a different container or position within the page.',
3025
+ inputSchema: {
3026
+ type: 'object',
3027
+ properties: {
3028
+ post_id: { type: 'number', description: 'Page/post ID' },
3029
+ element_id: { type: 'string', description: 'Element ID to move' },
3030
+ target_container_id: {
3031
+ type: 'string',
3032
+ description: 'ID of the container to move into',
3033
+ },
3034
+ position: {
3035
+ type: 'number',
3036
+ description: 'Position index within the target container (0-based)',
3037
+ },
3038
+ },
3039
+ required: ['post_id', 'element_id', 'target_container_id', 'position'],
3040
+ },
3041
+ },
3042
+ {
3043
+ name: 'wordpress_duplicate_element',
3044
+ description: 'Duplicate an element in a page. The copy is inserted immediately after the original.',
3045
+ inputSchema: {
3046
+ type: 'object',
3047
+ properties: {
3048
+ post_id: { type: 'number', description: 'Page/post ID' },
3049
+ element_id: { type: 'string', description: 'Element ID to duplicate' },
3050
+ },
3051
+ required: ['post_id', 'element_id'],
3052
+ },
3053
+ },
3054
+ {
3055
+ name: 'wordpress_remove_element',
3056
+ description: 'Remove an element from a page.',
3057
+ inputSchema: {
3058
+ type: 'object',
3059
+ properties: {
3060
+ post_id: { type: 'number', description: 'Page/post ID' },
3061
+ element_id: { type: 'string', description: 'Element ID to remove' },
3062
+ },
3063
+ required: ['post_id', 'element_id'],
3064
+ },
3065
+ destructiveHint: true,
3066
+ },
3067
+ {
3068
+ name: 'wordpress_batch_update',
3069
+ description: 'Apply multiple element operations to a page in a single atomic transaction. Extracts content once, applies all operations, and injects once.',
3070
+ inputSchema: {
3071
+ type: 'object',
3072
+ properties: {
3073
+ post_id: { type: 'number', description: 'Page/post ID' },
3074
+ operations: {
3075
+ type: 'array',
3076
+ description: 'Array of operations: [{action: "update"|"remove"|"move"|"duplicate", ...params}]',
3077
+ items: { type: 'object' },
3078
+ },
3079
+ },
3080
+ required: ['post_id', 'operations'],
3081
+ },
3082
+ },
3083
+ {
3084
+ name: 'wordpress_reorder_elements',
3085
+ description: 'Reorder child elements within a container.',
3086
+ inputSchema: {
3087
+ type: 'object',
3088
+ properties: {
3089
+ post_id: { type: 'number', description: 'Page/post ID' },
3090
+ container_id: { type: 'string', description: 'Container element ID' },
3091
+ new_order: {
3092
+ type: 'array',
3093
+ description: 'Array of element IDs in the desired order',
3094
+ items: { type: 'string' },
3095
+ },
3096
+ },
3097
+ required: ['post_id', 'container_id', 'new_order'],
3098
+ },
3099
+ },
3100
+ // --- Composite Tools ---
3101
+ {
3102
+ name: 'wordpress_build_page',
3103
+ description: 'Create a complete page from a declarative structure. Accepts builder-specific widget definitions and creates the page with all elements in one call. Returns page_id, edit_url, and preview_url.',
3104
+ inputSchema: {
3105
+ type: 'object',
3106
+ properties: {
3107
+ title: { type: 'string', description: 'Page title' },
3108
+ structure: {
3109
+ type: 'array',
3110
+ description: 'Array of widget definitions: [{type: "heading", settings: {title: "Hello"}}]',
3111
+ items: { type: 'object' },
3112
+ },
3113
+ status: {
3114
+ type: 'string',
3115
+ description: 'Page status',
3116
+ enum: ['draft', 'publish'],
3117
+ },
3118
+ builder: {
3119
+ type: 'string',
3120
+ description: 'Target builder (auto-detected if omitted)',
3121
+ },
3122
+ },
3123
+ required: ['title', 'structure'],
3124
+ },
3125
+ },
3126
+ {
3127
+ name: 'wordpress_convert_html_to_builder',
3128
+ description: 'Convert HTML into native page builder content. Extracts CSS styles, maps sections to builder widgets, and creates a page with a fidelity report showing conversion accuracy.',
3129
+ inputSchema: {
3130
+ type: 'object',
3131
+ properties: {
3132
+ html: { type: 'string', description: 'Full HTML string to convert' },
3133
+ builder: {
3134
+ type: 'string',
3135
+ description: 'Target builder (auto-detected if omitted)',
3136
+ },
3137
+ options: {
3138
+ type: 'object',
3139
+ description: 'Conversion options: {status, title, preserve_tokens, font_substitution, responsive}',
3140
+ },
3141
+ },
3142
+ required: ['html'],
3143
+ },
3144
+ },
3145
+ {
3146
+ name: 'wordpress_bulk_pages_operation',
3147
+ description: 'Apply an operation across up to 100 pages. Supports strip_inline_styles, find_and_replace, and custom operations. Each page gets a snapshot for rollback. Rate limited to 3/hr.',
3148
+ inputSchema: {
3149
+ type: 'object',
3150
+ properties: {
3151
+ page_ids: {
3152
+ type: 'array',
3153
+ description: 'Array of page IDs to process (max 100)',
3154
+ items: { type: 'number' },
3155
+ },
3156
+ operation: {
3157
+ type: 'object',
3158
+ description: 'Operation to apply: {type: "strip_inline_styles"|"find_and_replace"|"custom", options: {...}}',
3159
+ },
3160
+ options: {
3161
+ type: 'object',
3162
+ description: 'Execution options: {dry_run: boolean}',
3163
+ },
3164
+ },
3165
+ required: ['page_ids', 'operation'],
3166
+ },
3167
+ },
3168
+ // --- Stock Images ---
3169
+ {
3170
+ name: 'wordpress_search_stock_images',
3171
+ description: 'Search for free stock images via Openverse (Creative Commons). Returns image URLs, titles, authors, and licenses.',
3172
+ inputSchema: {
3173
+ type: 'object',
3174
+ properties: {
3175
+ query: { type: 'string', description: 'Search keywords' },
3176
+ license_type: {
3177
+ type: 'string',
3178
+ description: 'License filter',
3179
+ enum: ['commercial', 'modification', 'all'],
3180
+ },
3181
+ page: { type: 'number', description: 'Page number' },
3182
+ per_page: { type: 'number', description: 'Results per page (max 20)' },
3183
+ },
3184
+ required: ['query'],
3185
+ },
3186
+ readOnlyHint: true,
3187
+ },
3188
+ {
3189
+ name: 'wordpress_sideload_image',
3190
+ description: 'Download a stock image from an allowed domain and add it to the WordPress Media Library. Auto-generates CC attribution caption. Allowed domains: openverse.org, wordpress.org, unsplash.com, pexels.com, pixabay.com.',
3191
+ inputSchema: {
3192
+ type: 'object',
3193
+ properties: {
3194
+ url: { type: 'string', description: 'Image URL (HTTPS only, allowed domains only)' },
3195
+ title: { type: 'string', description: 'Attachment title' },
3196
+ alt: { type: 'string', description: 'Alt text' },
3197
+ caption: { type: 'string', description: 'Caption (auto-generated if omitted)' },
3198
+ },
3199
+ required: ['url'],
3200
+ },
3201
+ },
3202
+ // --- Server Compatibility ---
3203
+ {
3204
+ name: 'wordpress_get_server_compatibility',
3205
+ description: 'Check version compatibility between the MCP server and the WordPress plugin. Returns plugin version, supported version range, and available features.',
3206
+ inputSchema: {
3207
+ type: 'object',
3208
+ properties: {},
3209
+ },
3210
+ readOnlyHint: true,
3211
+ },
3212
+ // --- Widget Shortcuts (27 tools) ---
3213
+ ...this.getWidgetShortcutTools(),
3214
+ ];
3215
+ }
3216
+ /**
3217
+ * Generate tool definitions for all 27 widget shortcuts.
3218
+ */
3219
+ getWidgetShortcutTools() {
3220
+ const shortcuts = [
3221
+ {
3222
+ name: 'add_heading',
3223
+ widget: 'heading',
3224
+ description: 'Add a heading widget to a page.',
3225
+ properties: {
3226
+ title: { type: 'string', description: 'Heading text' },
3227
+ tag: { type: 'string', description: 'HTML tag (h1-h6)', enum: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] },
3228
+ alignment: { type: 'string', enum: ['left', 'center', 'right'] },
3229
+ },
3230
+ required: ['title'],
3231
+ },
3232
+ {
3233
+ name: 'add_text',
3234
+ widget: 'text-editor',
3235
+ description: 'Add a text/paragraph widget to a page.',
3236
+ properties: {
3237
+ content: { type: 'string', description: 'Text content (HTML allowed)' },
3238
+ alignment: { type: 'string', enum: ['left', 'center', 'right'] },
3239
+ },
3240
+ required: ['content'],
3241
+ },
3242
+ {
3243
+ name: 'add_button',
3244
+ widget: 'button',
3245
+ description: 'Add a button widget to a page.',
3246
+ properties: {
3247
+ text: { type: 'string', description: 'Button label' },
3248
+ url: { type: 'string', description: 'Link URL' },
3249
+ style: { type: 'string', enum: ['primary', 'secondary'] },
3250
+ alignment: { type: 'string', enum: ['left', 'center', 'right'] },
3251
+ },
3252
+ required: ['text'],
3253
+ },
3254
+ {
3255
+ name: 'add_image',
3256
+ widget: 'image',
3257
+ description: 'Add an image widget to a page.',
3258
+ properties: {
3259
+ src: { type: 'string', description: 'Image URL or media library attachment URL' },
3260
+ alt: { type: 'string', description: 'Alt text' },
3261
+ width: { type: 'string', description: 'Image width' },
3262
+ height: { type: 'string', description: 'Image height' },
3263
+ },
3264
+ required: ['src'],
3265
+ },
3266
+ {
3267
+ name: 'add_section',
3268
+ widget: 'section',
3269
+ description: 'Add a section container to a page.',
3270
+ properties: {
3271
+ background_color: { type: 'string', description: 'Background color (hex)' },
3272
+ padding: { type: 'string', description: 'CSS padding value' },
3273
+ },
3274
+ required: [],
3275
+ },
3276
+ {
3277
+ name: 'add_video',
3278
+ widget: 'video',
3279
+ description: 'Add a video widget to a page.',
3280
+ properties: {
3281
+ url: { type: 'string', description: 'Video URL (YouTube, Vimeo, or self-hosted)' },
3282
+ autoplay: { type: 'boolean', description: 'Auto-play video' },
3283
+ },
3284
+ required: ['url'],
3285
+ },
3286
+ {
3287
+ name: 'add_divider',
3288
+ widget: 'divider',
3289
+ description: 'Add a divider/horizontal rule to a page.',
3290
+ properties: {
3291
+ style: { type: 'string', enum: ['solid', 'dashed', 'dotted', 'double'] },
3292
+ color: { type: 'string', description: 'Divider color (hex)' },
3293
+ },
3294
+ required: [],
3295
+ },
3296
+ {
3297
+ name: 'add_spacer',
3298
+ widget: 'spacer',
3299
+ description: 'Add vertical spacing to a page.',
3300
+ properties: {
3301
+ height: { type: 'string', description: 'Spacer height (e.g. "50px")' },
3302
+ },
3303
+ required: [],
3304
+ },
3305
+ {
3306
+ name: 'add_icon',
3307
+ widget: 'icon',
3308
+ description: 'Add an icon widget to a page.',
3309
+ properties: {
3310
+ icon: { type: 'string', description: 'Icon name (e.g. "fa fa-star")' },
3311
+ size: { type: 'string', enum: ['sm', 'md', 'lg', 'xl'] },
3312
+ color: { type: 'string', description: 'Icon color (hex)' },
3313
+ },
3314
+ required: ['icon'],
3315
+ },
3316
+ {
3317
+ name: 'add_icon_list',
3318
+ widget: 'icon-list',
3319
+ description: 'Add an icon list widget to a page.',
3320
+ properties: {
3321
+ items: {
3322
+ type: 'array',
3323
+ description: 'List items: [{icon: "fa fa-check", text: "Item text"}]',
3324
+ items: { type: 'object' },
3325
+ },
3326
+ },
3327
+ required: ['items'],
3328
+ },
3329
+ {
3330
+ name: 'add_social_icons',
3331
+ widget: 'social-icons',
3332
+ description: 'Add social media icons to a page.',
3333
+ properties: {
3334
+ networks: {
3335
+ type: 'array',
3336
+ description: 'Social networks: [{network: "facebook", url: "..."}]',
3337
+ items: { type: 'object' },
3338
+ },
3339
+ },
3340
+ required: ['networks'],
3341
+ },
3342
+ {
3343
+ name: 'add_form',
3344
+ widget: 'form',
3345
+ description: 'Add a contact form to a page.',
3346
+ properties: {
3347
+ fields: {
3348
+ type: 'array',
3349
+ description: 'Form fields: [{type: "text", label: "Name", required: true}]',
3350
+ items: { type: 'object' },
3351
+ },
3352
+ submit_text: { type: 'string', description: 'Submit button text' },
3353
+ },
3354
+ required: ['fields'],
3355
+ },
3356
+ {
3357
+ name: 'add_map',
3358
+ widget: 'google-maps',
3359
+ description: 'Add a Google Maps widget to a page.',
3360
+ properties: {
3361
+ address: { type: 'string', description: 'Address or coordinates' },
3362
+ zoom: { type: 'number', description: 'Zoom level (1-20)' },
3363
+ },
3364
+ required: ['address'],
3365
+ },
3366
+ {
3367
+ name: 'add_counter',
3368
+ widget: 'counter',
3369
+ description: 'Add a number counter widget to a page.',
3370
+ properties: {
3371
+ number: { type: 'number', description: 'Target number to count to' },
3372
+ title: { type: 'string', description: 'Counter label' },
3373
+ prefix: { type: 'string', description: 'Prefix (e.g. "$")' },
3374
+ suffix: { type: 'string', description: 'Suffix (e.g. "+")' },
3375
+ },
3376
+ required: ['number'],
3377
+ },
3378
+ {
3379
+ name: 'add_progress_bar',
3380
+ widget: 'progress-bar',
3381
+ description: 'Add a progress bar widget to a page.',
3382
+ properties: {
3383
+ title: { type: 'string', description: 'Bar label' },
3384
+ percentage: { type: 'number', description: 'Progress percentage (0-100)' },
3385
+ color: { type: 'string', description: 'Bar color (hex)' },
3386
+ },
3387
+ required: ['title', 'percentage'],
3388
+ },
3389
+ {
3390
+ name: 'add_testimonial',
3391
+ widget: 'testimonial',
3392
+ description: 'Add a testimonial widget to a page.',
3393
+ properties: {
3394
+ content: { type: 'string', description: 'Testimonial text' },
3395
+ name: { type: 'string', description: 'Author name' },
3396
+ title: { type: 'string', description: 'Author title/role' },
3397
+ image: { type: 'string', description: 'Author photo URL' },
3398
+ },
3399
+ required: ['content', 'name'],
3400
+ },
3401
+ {
3402
+ name: 'add_tabs',
3403
+ widget: 'tabs',
3404
+ description: 'Add a tabbed content widget to a page.',
3405
+ properties: {
3406
+ tabs: {
3407
+ type: 'array',
3408
+ description: 'Tab definitions: [{title: "Tab 1", content: "Content..."}]',
3409
+ items: { type: 'object' },
3410
+ },
3411
+ },
3412
+ required: ['tabs'],
3413
+ },
3414
+ {
3415
+ name: 'add_accordion',
3416
+ widget: 'accordion',
3417
+ description: 'Add an accordion/FAQ widget to a page.',
3418
+ properties: {
3419
+ items: {
3420
+ type: 'array',
3421
+ description: 'Accordion items: [{title: "Question", content: "Answer"}]',
3422
+ items: { type: 'object' },
3423
+ },
3424
+ },
3425
+ required: ['items'],
3426
+ },
3427
+ {
3428
+ name: 'add_toggle',
3429
+ widget: 'toggle',
3430
+ description: 'Add a toggle (collapsible content) widget to a page.',
3431
+ properties: {
3432
+ title: { type: 'string', description: 'Toggle header text' },
3433
+ content: { type: 'string', description: 'Toggle body content' },
3434
+ },
3435
+ required: ['title', 'content'],
3436
+ },
3437
+ {
3438
+ name: 'add_alert',
3439
+ widget: 'alert',
3440
+ description: 'Add an alert/notice widget to a page.',
3441
+ properties: {
3442
+ message: { type: 'string', description: 'Alert message text' },
3443
+ type: { type: 'string', enum: ['info', 'success', 'warning', 'danger'] },
3444
+ dismissible: { type: 'boolean', description: 'Whether alert can be dismissed' },
3445
+ },
3446
+ required: ['message'],
3447
+ },
3448
+ {
3449
+ name: 'add_html',
3450
+ widget: 'html',
3451
+ description: 'Add a raw HTML widget to a page.',
3452
+ properties: {
3453
+ content: { type: 'string', description: 'Raw HTML content' },
3454
+ },
3455
+ required: ['content'],
3456
+ },
3457
+ {
3458
+ name: 'add_menu',
3459
+ widget: 'nav-menu',
3460
+ description: 'Add a navigation menu widget to a page.',
3461
+ properties: {
3462
+ menu_id: { type: 'number', description: 'WordPress menu ID' },
3463
+ },
3464
+ required: ['menu_id'],
3465
+ },
3466
+ {
3467
+ name: 'add_sidebar',
3468
+ widget: 'sidebar',
3469
+ description: 'Add a sidebar widget area to a page.',
3470
+ properties: {
3471
+ sidebar_id: { type: 'string', description: 'Sidebar/widget area ID' },
3472
+ },
3473
+ required: ['sidebar_id'],
3474
+ },
3475
+ {
3476
+ name: 'add_search',
3477
+ widget: 'search-form',
3478
+ description: 'Add a search form widget to a page.',
3479
+ properties: {
3480
+ placeholder: { type: 'string', description: 'Placeholder text' },
3481
+ },
3482
+ required: [],
3483
+ },
3484
+ {
3485
+ name: 'add_gallery',
3486
+ widget: 'gallery',
3487
+ description: 'Add an image gallery widget to a page.',
3488
+ properties: {
3489
+ images: {
3490
+ type: 'array',
3491
+ description: 'Array of image objects: [{url: "...", alt: "..."}]',
3492
+ items: { type: 'object' },
3493
+ },
3494
+ columns: { type: 'number', description: 'Number of columns (1-10)' },
3495
+ },
3496
+ required: ['images'],
3497
+ },
3498
+ {
3499
+ name: 'add_slider',
3500
+ widget: 'slides',
3501
+ description: 'Add an image/content slider to a page.',
3502
+ properties: {
3503
+ slides: {
3504
+ type: 'array',
3505
+ description: 'Slide definitions: [{image: "...", title: "...", description: "..."}]',
3506
+ items: { type: 'object' },
3507
+ },
3508
+ autoplay: { type: 'boolean', description: 'Auto-advance slides' },
3509
+ },
3510
+ required: ['slides'],
3511
+ },
3512
+ {
3513
+ name: 'add_pricing_table',
3514
+ widget: 'price-table',
3515
+ description: 'Add a pricing table widget to a page.',
3516
+ properties: {
3517
+ title: { type: 'string', description: 'Plan name' },
3518
+ price: { type: 'string', description: 'Price amount (e.g. "29")' },
3519
+ period: { type: 'string', description: 'Billing period (e.g. "/mo")' },
3520
+ features: {
3521
+ type: 'array',
3522
+ description: 'Feature list',
3523
+ items: { type: 'string' },
3524
+ },
3525
+ button_text: { type: 'string', description: 'CTA button text' },
3526
+ button_url: { type: 'string', description: 'CTA button URL' },
3527
+ },
3528
+ required: ['title', 'price'],
3529
+ },
3530
+ ];
3531
+ return shortcuts.map((s) => ({
3532
+ name: `wordpress_${s.name}`,
3533
+ description: s.description,
3534
+ inputSchema: {
3535
+ type: 'object',
3536
+ properties: {
3537
+ post_id: { type: 'number', description: 'Page/post ID to add the widget to' },
3538
+ ...s.properties,
3539
+ },
3540
+ required: ['post_id', ...s.required],
3541
+ },
3542
+ }));
3543
+ }
2684
3544
  async run() {
2685
3545
  const transport = new StdioServerTransport();
2686
3546
  await this.server.connect(transport);