PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.8
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.8
0.0.11 0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / lib / mcp-adapter / includes / Core / McpComponentRegistry.php

McpComponentRegistry.php in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.8, at lib/mcp-adapter/includes/Core/McpComponentRegistry.php

662 lines 18.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * MCP Component Registry for managing tools, resources, and prompts.
5 *
6 * @package McpAdapter
7 */
8
9 declare( strict_types=1 );
10
11 namespace WP\MCP\Core;
12
13 use WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface;
14 use WP\MCP\Domain\Prompts\McpPrompt;
15 use WP\MCP\Domain\Resources\McpResource;
16 use WP\MCP\Domain\Tools\McpTool;
17 use WP\MCP\Infrastructure\ErrorHandling\Contracts\McpErrorHandlerInterface;
18 use WP\MCP\Infrastructure\Observability\Contracts\McpObservabilityHandlerInterface;
19 use WP\MCP\Infrastructure\Observability\FailureReason;
20 use WP\McpSchema\Server\Prompts\DTO\Prompt as PromptDto;
21 use WP\McpSchema\Server\Resources\DTO\Resource as ResourceDto;
22 use WP\McpSchema\Server\Tools\DTO\Tool as ToolDto;
23 use WP_Error;
24
25 /**
26 * Registry for managing MCP server components (tools, resources, prompts).
27 */
28 class McpComponentRegistry {
29
30 /**
31 * MCP tools keyed by tool name.
32 *
33 * @var array<string, \WP\MCP\Domain\Tools\McpTool>
34 */
35 private array $mcp_tools = array();
36
37 /**
38 * MCP resources keyed by resource URI.
39 *
40 * @var array<string, \WP\MCP\Domain\Resources\McpResource>
41 */
42 private array $mcp_resources = array();
43
44 /**
45 * MCP prompts keyed by prompt name.
46 *
47 * @var array<string, \WP\MCP\Domain\Prompts\McpPrompt>
48 */
49 private array $mcp_prompts = array();
50
51 /**
52 * MCP Server instance.
53 *
54 * @var \WP\MCP\Core\McpServer
55 */
56 private McpServer $mcp_server;
57
58 /**
59 * Error handler instance.
60 *
61 * @var \WP\MCP\Infrastructure\ErrorHandling\Contracts\McpErrorHandlerInterface
62 */
63 private McpErrorHandlerInterface $error_handler;
64
65 /**
66 * Observability handler instance.
67 *
68 * @var \WP\MCP\Infrastructure\Observability\Contracts\McpObservabilityHandlerInterface
69 */
70 private McpObservabilityHandlerInterface $observability_handler;
71
72 /**
73 * Whether to record component registration.
74 *
75 * @var bool
76 */
77 private bool $should_record_component_registration;
78
79 /**
80 * Constructor.
81 *
82 * @param \WP\MCP\Core\McpServer $mcp_server MCP server instance.
83 * @param \WP\MCP\Infrastructure\ErrorHandling\Contracts\McpErrorHandlerInterface $error_handler Error handler instance.
84 * @param \WP\MCP\Infrastructure\Observability\Contracts\McpObservabilityHandlerInterface $observability_handler Observability handler instance.
85 */
86 public function __construct(
87 McpServer $mcp_server,
88 McpErrorHandlerInterface $error_handler,
89 McpObservabilityHandlerInterface $observability_handler
90 ) {
91 $this->mcp_server = $mcp_server;
92 $this->error_handler = $error_handler;
93 $this->observability_handler = $observability_handler;
94
95 /**
96 * Filters whether component registration events should be recorded for observability.
97 *
98 * Default is false to avoid polluting observability logs during startup.
99 * Enable this filter to track tool, resource, and prompt registrations
100 * for debugging or monitoring purposes.
101 *
102 * @since 0.3.0
103 *
104 * @param bool $should_record Whether to record component registration events. Default false.
105 * @param string $server_id The server ID for which components are being registered.
106 * @param \WP\MCP\Core\McpServer $server The McpServer instance owning the registry.
107 */
108 $this->should_record_component_registration = apply_filters(
109 'mcp_adapter_observability_record_component_registration',
110 false,
111 $this->mcp_server->get_server_id(),
112 $this->mcp_server
113 );
114 }
115
116 /**
117 * Register tools to the server.
118 *
119 * @param list<string|\WP\MCP\Domain\Tools\McpTool> $tools Array of ability names (strings) or McpTool instances.
120 *
121 * @return void
122 */
123 public function register_tools( array $tools ): void {
124 foreach ( $tools as $tool_item ) {
125 $this->register_single_tool( $tool_item );
126 }
127 }
128
129 /**
130 * Register a single tool to the server.
131 *
132 * @param string|\WP\MCP\Domain\Tools\McpTool $tool_item The tool to register.
133 *
134 * @return void
135 */
136 private function register_single_tool( $tool_item ): void {
137 // Case 0: McpTool instance.
138 if ( $tool_item instanceof McpTool ) {
139 $this->add_mcp_tool( $tool_item );
140
141 /** @var \WP\McpSchema\Server\Tools\DTO\Tool $tool_dto */
142 $tool_dto = $tool_item->get_protocol_dto();
143 $this->track_registration( 'tool', $tool_dto->getName(), 'success' );
144
145 return;
146 }
147
148 // Case 1: String - treat as ability name.
149 if ( is_string( $tool_item ) ) {
150 $this->register_ability_tool( $tool_item );
151
152 return;
153 }
154
155 $this->error_handler->log(
156 sprintf(
157 'Invalid tool registration item: expected McpTool instance or string ability name, got %s.',
158 is_object( $tool_item ) ? get_class( $tool_item ) : gettype( $tool_item )
159 ),
160 array( 'McpComponentRegistry::register_single_tool' ),
161 'warning'
162 );
163 }
164
165 /**
166 * Register an McpTool directly.
167 *
168 * @param \WP\MCP\Domain\Tools\McpTool $mcp_tool McpTool instance.
169 *
170 * @return void
171 * @since 0.3.0
172 *
173 */
174 private function add_mcp_tool( McpTool $mcp_tool ): void {
175 /** @var \WP\McpSchema\Server\Tools\DTO\Tool $tool_dto */
176 $tool_dto = $mcp_tool->get_protocol_dto();
177 $tool_name = $tool_dto->getName();
178
179 if ( isset( $this->mcp_tools[ $tool_name ] ) ) {
180 $this->error_handler->log(
181 "Tool with name '{$tool_name}' already registered, skipping duplicate.",
182 array( 'McpComponentRegistry::add_mcp_tool' ),
183 'warning'
184 );
185
186 return;
187 }
188
189 $this->mcp_tools[ $tool_name ] = $mcp_tool;
190 }
191
192 /**
193 * Record a component registration event.
194 *
195 * @param string $type Component type.
196 * @param string $name Component name.
197 * @param string $status Registration status ('success' or 'failed').
198 * @param array<string, mixed> $extra Extra event data.
199 *
200 * @return void
201 */
202 private function track_registration( string $type, string $name, string $status, array $extra = array() ): void {
203 if ( ! $this->should_record_component_registration ) {
204 return;
205 }
206
207 $event_data = array_merge(
208 array(
209 'status' => $status,
210 'component_type' => $type,
211 'component_name' => $name,
212 'server_id' => $this->mcp_server->get_server_id(),
213 ),
214 $extra
215 );
216
217 $this->observability_handler->record_event( 'mcp.component.registration', $event_data );
218 }
219
220 /**
221 * Register a tool from an ability name.
222 *
223 * @param string $ability_name Ability name.
224 *
225 * @return void
226 */
227 private function register_ability_tool( string $ability_name ): void {
228 $ability = \wp_get_ability( $ability_name );
229
230 if ( ! $ability ) {
231 $this->error_handler->log( "WordPress ability '{$ability_name}' does not exist.", array( "RegisterAbilityAsMcpTool::{$ability_name}" ) );
232 $this->track_registration( 'ability_tool', $ability_name, 'failed', array( 'failure_reason' => FailureReason::ABILITY_NOT_FOUND ) );
233
234 return;
235 }
236
237 $mcp_tool = McpTool::fromAbility( $ability );
238
239 if ( is_wp_error( $mcp_tool ) ) {
240 $this->error_handler->log( $mcp_tool->get_error_message(), array( "McpTool::fromAbility::{$ability_name}" ) );
241 $this->track_registration(
242 'ability_tool',
243 $ability_name,
244 'failed',
245 array( 'error_code' => $mcp_tool->get_error_code() )
246 );
247
248 return;
249 }
250
251 $this->add_mcp_tool( $mcp_tool );
252 $this->track_registration( 'ability_tool', $ability_name, 'success' );
253 }
254
255 /**
256 * Register resources to the server.
257 *
258 * @param list<string|\WP\MCP\Domain\Resources\McpResource> $resources Array of ability names or McpResource instances.
259 *
260 * @return void
261 */
262 public function register_resources( array $resources ): void {
263 foreach ( $resources as $resource_item ) {
264 $this->register_single_resource( $resource_item );
265 }
266 }
267
268 /**
269 * Register a single resource to the server.
270 *
271 * @param string|\WP\MCP\Domain\Resources\McpResource $resource_item The resource to register.
272 *
273 * @return void
274 */
275 private function register_single_resource( $resource_item ): void {
276 // Case 0: McpResource instance.
277 if ( $resource_item instanceof McpResource ) {
278 $this->add_mcp_resource( $resource_item );
279
280 /** @var \WP\McpSchema\Server\Resources\DTO\Resource $resource_dto */
281 $resource_dto = $resource_item->get_protocol_dto();
282 $this->track_registration( 'resource', $resource_dto->getUri(), 'success' );
283
284 return;
285 }
286
287 // Case 1: String - treat as ability name.
288 if ( is_string( $resource_item ) ) {
289 $this->register_ability_resource( $resource_item );
290
291 return;
292 }
293
294 // Case 2: Invalid type.
295 $this->error_handler->log(
296 sprintf(
297 'Invalid resource registration item: expected McpResource instance or string ability name, got %s.',
298 is_object( $resource_item ) ? get_class( $resource_item ) : gettype( $resource_item )
299 ),
300 array( 'McpComponentRegistry::register_single_resource' ),
301 'warning'
302 );
303 }
304
305 /**
306 * Register an McpResource directly.
307 *
308 * @param \WP\MCP\Domain\Resources\McpResource $mcp_resource McpResource instance.
309 *
310 * @return bool True if the resource was added, false if it was a duplicate.
311 * @since 0.3.0
312 *
313 */
314 private function add_mcp_resource( McpResource $mcp_resource ): bool {
315 /** @var \WP\McpSchema\Server\Resources\DTO\Resource $resource_dto */
316 $resource_dto = $mcp_resource->get_protocol_dto();
317 $uri = $resource_dto->getUri();
318
319 if ( isset( $this->mcp_resources[ $uri ] ) ) {
320 $this->error_handler->log(
321 "Resource with URI '{$uri}' already registered, skipping duplicate.",
322 array( 'McpComponentRegistry::add_mcp_resource' ),
323 'warning'
324 );
325
326 return false;
327 }
328
329 $this->mcp_resources[ $uri ] = $mcp_resource;
330
331 return true;
332 }
333
334 /**
335 * Register an ability-backed resource by ability name.
336 *
337 * @param string $ability_name Ability name.
338 *
339 * @return void
340 */
341 private function register_ability_resource( string $ability_name ): void {
342 $ability = \wp_get_ability( $ability_name );
343
344 if ( ! $ability ) {
345 $this->error_handler->log( "WordPress ability '{$ability_name}' does not exist.", array( "RegisterAbilityAsMcpResource::{$ability_name}" ) );
346
347 $this->track_registration( 'resource', $ability_name, 'failed', array( 'failure_reason' => FailureReason::ABILITY_NOT_FOUND ) );
348
349 return;
350 }
351
352 $mcp_resource = McpResource::fromAbility( $ability, $this->error_handler );
353
354 // Check if resource creation returned an error.
355 if ( is_wp_error( $mcp_resource ) ) {
356 $this->error_handler->log( $mcp_resource->get_error_message(), array( "McpResource::fromAbility::{$ability_name}" ) );
357
358 $this->track_registration(
359 'resource',
360 $ability_name,
361 'failed',
362 array( 'error_code' => $mcp_resource->get_error_code() )
363 );
364
365 return;
366 }
367
368 $added = $this->add_mcp_resource( $mcp_resource );
369
370 if ( $added ) {
371 $this->track_registration( 'resource', $ability_name, 'success' );
372 } else {
373 /** @var \WP\McpSchema\Server\Resources\DTO\Resource $resource_dto */
374 $resource_dto = $mcp_resource->get_protocol_dto();
375 $this->track_registration(
376 'resource',
377 $ability_name,
378 'failed',
379 array(
380 'failure_reason' => FailureReason::DUPLICATE_URI,
381 'duplicate_uri' => $resource_dto->getUri(),
382 )
383 );
384 }
385 }
386
387 /**
388 * Register prompts to the server.
389 *
390 * Accepts multiple formats:
391 * - McpPrompt instances
392 * - Class name string implementing McpPromptBuilderInterface (instantiated automatically)
393 * - Ability name string (converted via RegisterAbilityAsMcpPrompt)
394 * - McpPromptBuilderInterface instance (fluent API or custom builders)
395 * - Array configuration (converted via McpPrompt::fromArray())
396 *
397 * @param list<string|\WP\MCP\Domain\Prompts\McpPrompt|\WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface> $prompts Array of prompts to register.
398 *
399 * @return void
400 */
401 public function register_prompts( array $prompts ): void {
402 foreach ( $prompts as $prompt_item ) {
403 $this->register_single_prompt( $prompt_item );
404 }
405 }
406
407 /**
408 * Register a single prompt to the server.
409 *
410 * @param string|\WP\MCP\Domain\Prompts\McpPrompt|\WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface $prompt_item The prompt to register.
411 *
412 * @return void
413 */
414 private function register_single_prompt( $prompt_item ): void {
415 // Case 0: McpPrompt instance.
416 if ( $prompt_item instanceof McpPrompt ) {
417 $this->add_mcp_prompt( $prompt_item );
418
419 /** @var \WP\McpSchema\Server\Prompts\DTO\Prompt $prompt_dto */
420 $prompt_dto = $prompt_item->get_protocol_dto();
421 $this->track_registration( 'prompt', $prompt_dto->getName(), 'success' );
422
423 return;
424 }
425
426 // Case 1: McpPromptBuilderInterface instance (fluent API or custom builder).
427 if ( $prompt_item instanceof McpPromptBuilderInterface ) {
428 $this->register_builder_instance( $prompt_item );
429
430 return;
431 }
432
433 // Case 2: String - either a class name or ability name.
434 if ( is_string( $prompt_item ) ) {
435 // Check if it's a class that implements McpPromptBuilderInterface.
436 if ( class_exists( $prompt_item ) && in_array( McpPromptBuilderInterface::class, class_implements( $prompt_item ) ?: array(), true ) ) {
437 $this->register_builder_class( $prompt_item );
438
439 return;
440 }
441
442 // Treat as ability name.
443 $this->register_ability_prompt( $prompt_item );
444
445 return;
446 }
447
448 // Case 3: Invalid type.
449 $this->error_handler->log(
450 sprintf(
451 'Invalid prompt registration item: expected McpPrompt, McpPromptBuilderInterface, or string, got %s.',
452 is_object( $prompt_item ) ? get_class( $prompt_item ) : gettype( $prompt_item )
453 ),
454 array( 'McpComponentRegistry::register_single_prompt' ),
455 'warning'
456 );
457 }
458
459 /**
460 * Add an McpPrompt to the registry.
461 *
462 * @param \WP\MCP\Domain\Prompts\McpPrompt $mcp_prompt McpPrompt instance.
463 *
464 * @return void
465 * @since 0.3.0
466 *
467 */
468 private function add_mcp_prompt( McpPrompt $mcp_prompt ): void {
469 /** @var \WP\McpSchema\Server\Prompts\DTO\Prompt $prompt */
470 $prompt = $mcp_prompt->get_protocol_dto();
471 $prompt_name = $prompt->getName();
472
473 if ( isset( $this->mcp_prompts[ $prompt_name ] ) ) {
474 $this->error_handler->log(
475 "Prompt with name '{$prompt_name}' already registered, skipping duplicate.",
476 array( 'McpComponentRegistry::add_mcp_prompt' ),
477 'warning'
478 );
479
480 return;
481 }
482
483 $this->mcp_prompts[ $prompt_name ] = $mcp_prompt;
484 }
485
486 /**
487 * Register a McpPromptBuilderInterface instance.
488 *
489 * @param \WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface $builder The builder instance.
490 *
491 * @return void
492 */
493 private function register_builder_instance( McpPromptBuilderInterface $builder ): void {
494 $prompt_name = $builder->get_name();
495
496 $mcp_prompt = McpPrompt::fromBuilder( $builder );
497 if ( $mcp_prompt instanceof WP_Error ) {
498 $this->error_handler->log( $mcp_prompt->get_error_message(), array( "McpPrompt::fromBuilder::{$prompt_name}" ) );
499
500 $this->track_registration(
501 'prompt',
502 $prompt_name,
503 'failed',
504 array( 'error_code' => $mcp_prompt->get_error_code() )
505 );
506
507 return;
508 }
509
510 $this->add_mcp_prompt( $mcp_prompt );
511
512 $this->track_registration( 'prompt', $prompt_name, 'success' );
513 }
514
515 /**
516 * Register a prompt from a builder class name.
517 *
518 * @param string $class_name The fully-qualified class name.
519 *
520 * @return void
521 */
522 private function register_builder_class( string $class_name ): void {
523 try {
524 /** @var \WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface $builder */
525 $builder = new $class_name();
526 $this->register_builder_instance( $builder );
527 } catch ( \Throwable $e ) {
528 $this->error_handler->log( "Failed to build prompt from class '{$class_name}': {$e->getMessage()}", array( "McpPromptBuilder::{$class_name}" ) );
529
530 $this->track_registration( 'prompt', $class_name, 'failed', array( 'failure_reason' => FailureReason::BUILDER_EXCEPTION ) );
531 }
532 }
533
534 /**
535 * Register a prompt from an ability name.
536 *
537 * @param string $ability_name The ability name.
538 *
539 * @return void
540 */
541 private function register_ability_prompt( string $ability_name ): void {
542 $ability = \wp_get_ability( $ability_name );
543
544 if ( ! $ability ) {
545 $this->error_handler->log( "WordPress ability '{$ability_name}' does not exist.", array( "RegisterAbilityAsMcpPrompt::{$ability_name}" ) );
546
547 $this->track_registration( 'prompt', $ability_name, 'failed', array( 'failure_reason' => FailureReason::ABILITY_NOT_FOUND ) );
548
549 return;
550 }
551
552 $mcp_prompt = McpPrompt::fromAbility( $ability );
553
554 if ( is_wp_error( $mcp_prompt ) ) {
555 $this->error_handler->log( $mcp_prompt->get_error_message(), array( "McpPrompt::fromAbility::{$ability_name}" ) );
556
557 $this->track_registration(
558 'prompt',
559 $ability_name,
560 'failed',
561 array( 'error_code' => $mcp_prompt->get_error_code() )
562 );
563
564 return;
565 }
566
567 $this->add_mcp_prompt( $mcp_prompt );
568
569 $this->track_registration( 'prompt', $ability_name, 'success' );
570 }
571
572 /**
573 * Get all tools registered to the server.
574 *
575 * @return array<string, \WP\McpSchema\Server\Tools\DTO\Tool>
576 */
577 public function get_tools(): array {
578 return array_map(
579 static fn( McpTool $mcp_tool ): ToolDto => $mcp_tool->get_protocol_dto(),
580 $this->mcp_tools
581 );
582 }
583
584 /**
585 * Get all resources registered to the server.
586 *
587 * @return array<string, \WP\McpSchema\Server\Resources\DTO\Resource>
588 */
589 public function get_resources(): array {
590 return array_map(
591 static fn( McpResource $mcp_resource ): ResourceDto => $mcp_resource->get_protocol_dto(),
592 $this->mcp_resources
593 );
594 }
595
596 /**
597 * Get all prompts registered to the server.
598 *
599 * @return array<string, \WP\McpSchema\Server\Prompts\DTO\Prompt>
600 */
601 public function get_prompts(): array {
602 return array_map(
603 static fn( McpPrompt $mcp_prompt ): PromptDto => $mcp_prompt->get_protocol_dto(),
604 $this->mcp_prompts
605 );
606 }
607
608 /**
609 * Get a specific McpTool by tool name.
610 *
611 * @param string $tool_name Tool name.
612 *
613 * @return \WP\MCP\Domain\Tools\McpTool|null
614 * @since 0.3.0
615 *
616 */
617 public function get_mcp_tool( string $tool_name ): ?McpTool {
618 return $this->mcp_tools[ $tool_name ] ?? null;
619 }
620
621 /**
622 * Get a specific McpResource by URI.
623 *
624 * @param string $resource_uri Resource URI.
625 *
626 * @return \WP\MCP\Domain\Resources\McpResource|null
627 * @internal
628 * @since 0.3.0
629 *
630 */
631 public function get_mcp_resource( string $resource_uri ): ?McpResource {
632 return $this->mcp_resources[ $resource_uri ] ?? null;
633 }
634
635 /**
636 * Get an McpPrompt by prompt name.
637 *
638 * @param string $prompt_name Prompt name.
639 *
640 * @return \WP\MCP\Domain\Prompts\McpPrompt|null
641 * @internal
642 * @since 0.3.0
643 *
644 */
645 public function get_mcp_prompt( string $prompt_name ): ?McpPrompt {
646 return $this->mcp_prompts[ $prompt_name ] ?? null;
647 }
648
649 /**
650 * Get a prompt builder instance by prompt name (builder-based prompts).
651 *
652 * @param string $prompt_name Prompt name.
653 *
654 * @return \WP\MCP\Domain\Prompts\Contracts\McpPromptBuilderInterface|null
655 */
656 public function get_prompt_builder( string $prompt_name ): ?McpPromptBuilderInterface {
657 $mcp_prompt = $this->mcp_prompts[ $prompt_name ] ?? null;
658
659 return $mcp_prompt ? $mcp_prompt->get_builder() : null;
660 }
661 }
662