PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.17
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.17
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / wp-mcp-server / tools / class-mcp-tool-plugin-settings.php

class-mcp-tool-plugin-settings.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.17, at wp-mcp-server/tools/class-mcp-tool-plugin-settings.php

656 lines 24.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP Tool: Plugin Settings Management
4 *
5 * Provides tools for viewing and managing WordPress plugin settings.
6 * This allows AI agents to read and update all MetaSync plugin configuration.
7 *
8 * @package MetaSync
9 * @subpackage MCP_Server/Tools
10 */
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 /**
17 * Get Plugin Settings Tool
18 */
19 class MCP_Tool_Get_Plugin_Settings extends MCP_Tool_Base {
20
21 public function get_name() {
22 return 'wordpress_get_plugin_settings';
23 }
24
25 public function get_description() {
26 return 'Get all plugin settings or settings for a specific section. Returns the complete plugin configuration including features, SEO controls, API keys, and more.';
27 }
28
29 public function get_input_schema() {
30 return [
31 'type' => 'object',
32 'properties' => [
33 'section' => [
34 'type' => 'string',
35 'description' => 'Optional: specific settings section to retrieve (e.g., "general", "whitelabel", "seo"). If omitted, returns all settings.',
36 'enum' => [
37 'general',
38 'whitelabel',
39 'seo',
40 'social',
41 'advanced',
42 'features',
43 'api',
44 'all'
45 ]
46 ],
47 'keys' => [
48 'type' => 'array',
49 'description' => 'Optional: specific setting keys to retrieve. If provided, only these keys will be returned.',
50 'items' => [
51 'type' => 'string'
52 ]
53 ]
54 ],
55 'required' => []
56 ];
57 }
58
59 public function execute($params) {
60 // Validate and check permissions
61 $this->validate_params($params);
62 $this->require_capability('manage_options');
63
64 // Get all plugin options
65 $all_options = get_option(Metasync::option_name, []);
66
67 // If specific keys requested
68 if (!empty($params['keys']) && is_array($params['keys'])) {
69 $result = [];
70 foreach ($params['keys'] as $key) {
71 $key = $this->sanitize_string($key);
72 if (isset($all_options[$key])) {
73 $result[$key] = $all_options[$key];
74 }
75 }
76 return $this->success([
77 'settings' => $result,
78 'count' => count($result)
79 ]);
80 }
81
82 // If section specified (options are nested e.g. $all_options['general']['apikey'])
83 if (!empty($params['section']) && $params['section'] !== 'all') {
84 $section = $this->sanitize_string($params['section']);
85
86 if (isset($all_options[$section]) && is_array($all_options[$section])) {
87 $result = $all_options[$section];
88 } else {
89 // Fallback: map section to flat keys for legacy structure
90 $section_keys = $this->get_section_keys($section);
91 $result = [];
92 foreach ($section_keys as $key) {
93 if (isset($all_options[$key])) {
94 $result[$key] = $all_options[$key];
95 }
96 }
97 }
98
99 return $this->success([
100 'section' => $section,
101 'settings' => $result,
102 'count' => is_array($result) ? count($result) : 0
103 ]);
104 }
105
106 // Return all settings
107 return $this->success([
108 'settings' => $all_options,
109 'count' => count($all_options),
110 'available_sections' => [
111 'general' => 'API keys, integration settings',
112 'whitelabel' => 'Branding and white-label configuration',
113 'seo' => 'SEO controls and indexation settings',
114 'social' => 'Social media and OpenGraph settings',
115 'advanced' => 'Advanced plugin features',
116 'features' => 'Feature toggles and visibility',
117 'api' => 'API configuration and tokens'
118 ]
119 ]);
120 }
121
122 /**
123 * Get setting keys for a specific section
124 */
125 private function get_section_keys($section) {
126 $section_map = [
127 'general' => [
128 'searchatlas_api_key',
129 'apikey',
130 'permalink_structure',
131 'hide_dashboard_framework',
132 'show_admin_bar_status',
133 'enable_auto_updates',
134 'enable_schema_markup',
135 'default_schema_type'
136 ],
137 'whitelabel' => [
138 'white_label_plugin_name',
139 'whitelabel_otto_name',
140 'whitelabel_logo_url',
141 'whitelabel_domain_url',
142 'white_label_plugin_description',
143 'white_label_plugin_author',
144 'white_label_plugin_author_uri',
145 'white_label_plugin_uri',
146 'white_label_plugin_menu_slug',
147 'white_label_plugin_menu_icon',
148 'whitelabel_settings_password'
149 ],
150 'seo' => [
151 'index_date_archives',
152 'index_tag_archives',
153 'index_author_archives',
154 'index_format_archives',
155 'index_category_archives',
156 'override_robots_tags',
157 'enable_googleinstantindex',
158 'google_index_api_config'
159 ],
160 'social' => [
161 'otto_pixel_uuid',
162 'otto_disable_on_loggedin',
163 'otto_disable_preview_button',
164 'periodic_clear_ottopage_cache',
165 'periodic_clear_ottopost_cache',
166 'periodic_clear_otto_cache'
167 ],
168 'advanced' => [
169 'disable_common_robots_metabox',
170 'disable_advance_robots_metabox',
171 'disable_redirection_metabox',
172 'disable_canonical_metabox',
173 'disable_social_opengraph_metabox',
174 'disable_schema_markup_metabox',
175 'disable_seo_metabox'
176 ],
177 'features' => [
178 'enabled_elementor_plugin_css',
179 'enabled_elementor_plugin_css_color',
180 'import_external_data',
181 'content_genius_sync_roles'
182 ],
183 'api' => [
184 'searchatlas_api_key',
185 'apikey',
186 'google_index_api_config'
187 ]
188 ];
189
190 return $section_map[$section] ?? [];
191 }
192 }
193
194 /**
195 * Update Plugin Settings Tool
196 */
197 class MCP_Tool_Update_Plugin_Settings extends MCP_Tool_Base {
198
199 public function get_name() {
200 return 'wordpress_update_plugin_settings';
201 }
202
203 public function get_description() {
204 return 'Update one or more plugin settings. Allows modifying plugin configuration including features, SEO controls, API keys, whitelabel settings, and more.';
205 }
206
207 public function get_input_schema() {
208 return [
209 'type' => 'object',
210 'properties' => [
211 'settings' => [
212 'type' => 'object',
213 'description' => 'Key-value pairs of settings to update. Each key is a setting name and value is the new value.',
214 'additionalProperties' => true
215 ],
216 'merge' => [
217 'type' => 'boolean',
218 'description' => 'If true (default), merges with existing settings. If false, replaces all settings with provided values.',
219 'default' => true
220 ]
221 ],
222 'required' => ['settings']
223 ];
224 }
225
226 public function execute($params) {
227 // Validate and check permissions
228 $this->validate_params($params);
229 $this->require_capability('manage_options');
230
231 if (empty($params['settings']) || !is_array($params['settings'])) {
232 throw new Exception('Settings must be a non-empty object/array');
233 }
234
235 $merge = isset($params['merge']) ? (bool)$params['merge'] : true;
236
237 // Resolve any aliased or misnamed keys to their canonical nested paths
238 $settings = $this->resolve_setting_aliases($params['settings']);
239
240 // Get current options
241 $current_options = get_option(Metasync::option_name, []);
242
243 if ($merge) {
244 // Deep merge to preserve nested sub-arrays (e.g. general, seo_controls)
245 $new_options = $this->array_merge_deep($current_options, $settings);
246 } else {
247 // Replace all settings
248 $new_options = $settings;
249 }
250
251 // Sanitize sensitive fields
252 $new_options = $this->sanitize_settings($new_options);
253
254 // The whitelabel settings password is encrypted at rest —
255 // never persist a plaintext value. encrypt_secret() is a no-op on
256 // values that are already encrypted.
257 if (!empty($new_options['whitelabel']['settings_password']) && is_string($new_options['whitelabel']['settings_password'])) {
258 $new_options['whitelabel']['settings_password'] = Metasync::encrypt_secret($new_options['whitelabel']['settings_password']);
259 }
260
261 // Update the option
262 $updated = update_option(Metasync::option_name, $new_options);
263
264 if ($updated === false && $current_options !== $new_options) {
265 throw new Exception('Failed to update plugin settings');
266 }
267
268 // Clear any relevant caches
269 wp_cache_delete(Metasync::option_name, 'options');
270
271 return $this->success([
272 'message' => 'Plugin settings updated successfully',
273 'updated_keys' => array_keys($params['settings']),
274 'merge_mode' => $merge,
275 'total_settings' => count($new_options)
276 ]);
277 }
278
279 /**
280 * Resolve aliased or flat keys to their canonical nested location.
281 *
282 * When an MCP consumer sends a flat key like "enable_schema_markup", this
283 * method routes it into the correct nested sub-array ("general") so it
284 * updates the same option the admin UI uses.
285 */
286 private function resolve_setting_aliases($settings) {
287 // Map of flat keys → [section, canonical_key]
288 $nested_key_map = [
289 'enable_schema_markup' => ['general', 'enable_schema_markup'],
290 'enable_schema' => ['general', 'enable_schema_markup'],
291 'default_schema_type' => ['general', 'default_schema_type'],
292 ];
293
294 $resolved = [];
295 foreach ($settings as $key => $value) {
296 if (isset($nested_key_map[$key])) {
297 list($section, $canonical) = $nested_key_map[$key];
298 if (!isset($resolved[$section])) {
299 $resolved[$section] = [];
300 }
301 $resolved[$section][$canonical] = $value;
302 } else {
303 $resolved[$key] = $value;
304 }
305 }
306
307 return $resolved;
308 }
309
310 /**
311 * Recursively merge two arrays, preserving nested sub-arrays.
312 */
313 private function array_merge_deep($base, $override) {
314 foreach ($override as $key => $value) {
315 if (is_array($value) && isset($base[$key]) && is_array($base[$key])) {
316 $base[$key] = $this->array_merge_deep($base[$key], $value);
317 } else {
318 $base[$key] = $value;
319 }
320 }
321 return $base;
322 }
323
324 /**
325 * Sanitize settings before saving
326 */
327 private function sanitize_settings($settings) {
328 $sanitized = [];
329
330 foreach ($settings as $key => $value) {
331 $key = sanitize_key($key);
332
333 // Handle different types of values
334 if (is_array($value)) {
335 $sanitized[$key] = $this->sanitize_array($value);
336 } elseif (is_bool($value)) {
337 $sanitized[$key] = (bool)$value;
338 } elseif (is_numeric($value)) {
339 $sanitized[$key] = $value;
340 } elseif ($this->is_sensitive_field($key)) {
341 // Don't sanitize sensitive fields too aggressively
342 $sanitized[$key] = $value;
343 } elseif ($this->is_url_field($key)) {
344 $sanitized[$key] = esc_url_raw($value);
345 } else {
346 $sanitized[$key] = sanitize_text_field($value);
347 }
348 }
349
350 return $sanitized;
351 }
352
353 /**
354 * Sanitize array values recursively
355 */
356 private function sanitize_array($array) {
357 $sanitized = [];
358 foreach ($array as $key => $value) {
359 $key = sanitize_key($key);
360 if (is_array($value)) {
361 $sanitized[$key] = $this->sanitize_array($value);
362 } elseif (is_bool($value)) {
363 $sanitized[$key] = $value;
364 } elseif (is_numeric($value)) {
365 $sanitized[$key] = $value;
366 } else {
367 $sanitized[$key] = sanitize_text_field($value);
368 }
369 }
370 return $sanitized;
371 }
372
373 /**
374 * Check if field is sensitive (API keys, tokens, passwords)
375 */
376 private function is_sensitive_field($key) {
377 $sensitive_patterns = ['api_key', 'apikey', 'token', 'password', 'secret'];
378 foreach ($sensitive_patterns as $pattern) {
379 if (stripos($key, $pattern) !== false) {
380 return true;
381 }
382 }
383 return false;
384 }
385
386 /**
387 * Check if field should be a URL
388 */
389 private function is_url_field($key) {
390 $url_patterns = ['url', 'uri', 'domain', 'logo', 'link'];
391 foreach ($url_patterns as $pattern) {
392 if (stripos($key, $pattern) !== false) {
393 return true;
394 }
395 }
396 return false;
397 }
398 }
399
400 /**
401 * List Available Settings Tool
402 */
403 class MCP_Tool_List_Plugin_Settings_Schema extends MCP_Tool_Base {
404
405 public function get_name() {
406 return 'wordpress_list_plugin_settings_schema';
407 }
408
409 public function get_description() {
410 return 'Get a schema of all available plugin settings with descriptions and expected types. Useful for understanding what settings can be configured.';
411 }
412
413 public function get_input_schema() {
414 return [
415 'type' => 'object',
416 'properties' => (object)[],
417 'required' => []
418 ];
419 }
420
421 public function execute($params) {
422 $this->validate_params($params);
423 $this->require_capability('manage_options');
424
425 $schema = [
426 'general' => [
427 'description' => 'General plugin configuration',
428 'settings' => [
429 'searchatlas_api_key' => [
430 'type' => 'string',
431 'description' => 'Search Atlas API key for integration',
432 'sensitive' => true
433 ],
434 'apikey' => [
435 'type' => 'string',
436 'description' => 'Plugin authentication token',
437 'sensitive' => true
438 ],
439 'hide_dashboard_framework' => [
440 'type' => 'boolean',
441 'description' => 'Hide the dashboard framework'
442 ],
443 'show_admin_bar_status' => [
444 'type' => 'boolean',
445 'description' => 'Show plugin status in admin bar'
446 ],
447 'enable_auto_updates' => [
448 'type' => 'boolean',
449 'description' => 'Enable automatic plugin updates'
450 ],
451 'enable_schema_markup' => [
452 'type' => 'boolean',
453 'description' => 'Enable automatic schema markup generation. Canonical key — do NOT use "enable_schema".'
454 ],
455 'default_schema_type' => [
456 'type' => 'string',
457 'description' => 'Default schema type for new posts (e.g., "article", "WebSite")'
458 ]
459 ]
460 ],
461 'whitelabel' => [
462 'description' => 'White-label branding configuration',
463 'settings' => [
464 'white_label_plugin_name' => [
465 'type' => 'string',
466 'description' => 'Custom plugin name'
467 ],
468 'whitelabel_otto_name' => [
469 'type' => 'string',
470 'description' => 'Custom Otto feature name'
471 ],
472 'whitelabel_logo_url' => [
473 'type' => 'string',
474 'description' => 'URL to custom logo image'
475 ],
476 'whitelabel_domain_url' => [
477 'type' => 'string',
478 'description' => 'Custom dashboard domain URL'
479 ],
480 'white_label_plugin_description' => [
481 'type' => 'string',
482 'description' => 'Custom plugin description'
483 ],
484 'white_label_plugin_author' => [
485 'type' => 'string',
486 'description' => 'Custom author name'
487 ],
488 'white_label_plugin_author_uri' => [
489 'type' => 'string',
490 'description' => 'Custom author URI'
491 ],
492 'white_label_plugin_uri' => [
493 'type' => 'string',
494 'description' => 'Custom plugin URI'
495 ]
496 ]
497 ],
498 'seo' => [
499 'description' => 'SEO controls and indexation settings',
500 'settings' => [
501 'index_date_archives' => [
502 'type' => 'boolean',
503 'description' => 'Disallow date archives from indexation'
504 ],
505 'index_tag_archives' => [
506 'type' => 'boolean',
507 'description' => 'Disallow tag archives from indexation'
508 ],
509 'index_author_archives' => [
510 'type' => 'boolean',
511 'description' => 'Disallow author archives from indexation'
512 ],
513 'index_format_archives' => [
514 'type' => 'boolean',
515 'description' => 'Disallow format archives from indexation'
516 ],
517 'index_category_archives' => [
518 'type' => 'boolean',
519 'description' => 'Disallow category archives from indexation'
520 ],
521 'override_robots_tags' => [
522 'type' => 'boolean',
523 'description' => 'Override robots tags from other plugins'
524 ],
525 'enable_googleinstantindex' => [
526 'type' => 'boolean',
527 'description' => 'Enable Google Instant Indexing'
528 ]
529 ]
530 ],
531 'social' => [
532 'description' => 'Social media and Otto settings',
533 'settings' => [
534 'otto_pixel_uuid' => [
535 'type' => 'string',
536 'description' => 'Otto Pixel UUID for tracking'
537 ],
538 'otto_disable_on_loggedin' => [
539 'type' => 'boolean',
540 'description' => 'Disable Otto for logged-in users'
541 ],
542 'otto_disable_preview_button' => [
543 'type' => 'boolean',
544 'description' => 'Disable Otto frontend toolbar'
545 ]
546 ]
547 ],
548 'advanced' => [
549 'description' => 'Advanced meta box visibility controls',
550 'settings' => [
551 'disable_common_robots_metabox' => [
552 'type' => 'boolean',
553 'description' => 'Disable common robots meta box in post editor'
554 ],
555 'disable_advance_robots_metabox' => [
556 'type' => 'boolean',
557 'description' => 'Disable advanced robots meta box in post editor'
558 ],
559 'disable_redirection_metabox' => [
560 'type' => 'boolean',
561 'description' => 'Disable redirection meta box in post editor'
562 ],
563 'disable_canonical_metabox' => [
564 'type' => 'boolean',
565 'description' => 'Disable canonical meta box in post editor'
566 ],
567 'disable_social_opengraph_metabox' => [
568 'type' => 'boolean',
569 'description' => 'Disable social/OpenGraph meta box in post editor'
570 ],
571 'disable_schema_markup_metabox' => [
572 'type' => 'boolean',
573 'description' => 'Disable schema markup meta box in post editor'
574 ],
575 'disable_seo_metabox' => [
576 'type' => 'boolean',
577 'description' => 'Disable SEO Title & Meta Description meta box in Classic editor'
578 ]
579 ]
580 ],
581 'mcp' => [
582 'description' => 'MCP Server configuration',
583 'settings' => [
584 'metasync_mcp_enabled' => [
585 'type' => 'boolean',
586 'description' => 'Enable/disable the MCP server',
587 'stored_separately' => true,
588 'option_name' => 'metasync_mcp_enabled'
589 ],
590 'metasync_mcp_api_key' => [
591 'type' => 'string',
592 'description' => 'MCP server API key for authentication',
593 'sensitive' => true,
594 'stored_separately' => true,
595 'option_name' => 'metasync_mcp_api_key'
596 ]
597 ]
598 ]
599 ];
600
601 return $this->success([
602 'schema' => $schema,
603 'sections' => array_keys($schema),
604 'total_sections' => count($schema),
605 'note' => 'Some settings like MCP configuration are stored as separate options, not in the main metasync_options array'
606 ]);
607 }
608 }
609
610 /**
611 * Get MCP Server Settings Tool
612 */
613 class MCP_Tool_Get_MCP_Settings extends MCP_Tool_Base {
614
615 public function get_name() {
616 return 'wordpress_get_mcp_settings';
617 }
618
619 public function get_description() {
620 return 'Get MCP server-specific settings including authentication information and endpoint URLs.';
621 }
622
623 public function get_input_schema() {
624 return [
625 'type' => 'object',
626 'properties' => (object)[],
627 'required' => []
628 ];
629 }
630
631 public function execute($params) {
632 $this->validate_params($params);
633 $this->require_capability('manage_options');
634
635 $options = get_option('metasync_options', []);
636 $plugin_auth_token = isset($options['general']['apikey']) ? $options['general']['apikey'] : '';
637
638 return $this->success([
639 'mcp_enabled' => true, // MCP is always enabled
640 'authentication' => [
641 'type' => 'plugin_auth_token',
642 'header' => 'X-API-Key',
643 'token_set' => !empty($plugin_auth_token),
644 'token_length' => !empty($plugin_auth_token) ? strlen($plugin_auth_token) : 0,
645 'token_preview' => !empty($plugin_auth_token) ? substr($plugin_auth_token, 0, 8) . '...' : ''
646 ],
647 'endpoints' => [
648 'rest_endpoint' => rest_url('metasync/v1/mcp'),
649 'health_endpoint' => rest_url('metasync/v1/mcp/health')
650 ],
651 'version' => METASYNC_VERSION
652 ]);
653 }
654 }
655
656