|
12 | 12 | namespace Mcp\Tests\Unit\Capability; |
13 | 13 |
|
14 | 14 | use Mcp\Capability\Completion\EnumCompletionProvider; |
| 15 | +use Mcp\Capability\Discovery\DiscoveryState; |
15 | 16 | use Mcp\Capability\Registry; |
16 | 17 | use Mcp\Capability\Registry\PromptReference; |
17 | 18 | use Mcp\Capability\Registry\ResourceReference; |
@@ -426,42 +427,70 @@ public function testClearRemovesOnlyDiscoveredElements(): void |
426 | 427 | $manualTemplate = $this->createValidResourceTemplate('manual://{id}'); |
427 | 428 | $discoveredTemplate = $this->createValidResourceTemplate('discovered://{id}'); |
428 | 429 |
|
| 430 | + // Register manual elements directly |
429 | 431 | $this->registry->registerTool($manualTool, static fn () => 'manual', true); |
430 | | - $this->registry->registerTool($discoveredTool, static fn () => 'discovered'); |
431 | 432 | $this->registry->registerResource($manualResource, static fn () => 'manual', true); |
432 | | - $this->registry->registerResource($discoveredResource, static fn () => 'discovered'); |
433 | 433 | $this->registry->registerPrompt($manualPrompt, static fn () => [], [], true); |
434 | | - $this->registry->registerPrompt($discoveredPrompt, static fn () => []); |
435 | 434 | $this->registry->registerResourceTemplate($manualTemplate, static fn () => 'manual', [], true); |
436 | | - $this->registry->registerResourceTemplate($discoveredTemplate, static fn () => 'discovered'); |
437 | 435 |
|
438 | | - // Test that all elements exist |
439 | | - $this->registry->getTool('manual_tool'); |
440 | | - $this->registry->getResource('test://manual'); |
441 | | - $this->registry->getPrompt('manual_prompt'); |
442 | | - $this->registry->getResourceTemplate('manual://{id}'); |
443 | | - $this->registry->getTool('discovered_tool'); |
444 | | - $this->registry->getResource('test://discovered'); |
445 | | - $this->registry->getPrompt('discovered_prompt'); |
446 | | - $this->registry->getResourceTemplate('discovered://{id}'); |
| 436 | + // Import discovered elements via setDiscoveryState |
| 437 | + $this->registry->setDiscoveryState(new DiscoveryState( |
| 438 | + tools: ['discovered_tool' => new ToolReference($discoveredTool, static fn () => 'discovered')], |
| 439 | + resources: ['test://discovered' => new ResourceReference($discoveredResource, static fn () => 'discovered')], |
| 440 | + prompts: ['discovered_prompt' => new PromptReference($discoveredPrompt, static fn () => [])], |
| 441 | + resourceTemplates: ['discovered://{id}' => new ResourceTemplateReference($discoveredTemplate, static fn () => 'discovered')], |
| 442 | + )); |
| 443 | + |
| 444 | + // All elements exist before clear |
| 445 | + $this->assertInstanceOf(ToolReference::class, $this->registry->getTool('discovered_tool')); |
447 | 446 |
|
448 | 447 | $this->registry->clear(); |
449 | 448 |
|
450 | | - // Manual elements should still exist |
451 | | - $this->registry->getTool('manual_tool'); |
452 | | - $this->registry->getResource('test://manual'); |
453 | | - $this->registry->getPrompt('manual_prompt'); |
454 | | - $this->registry->getResourceTemplate('manual://{id}'); |
| 449 | + // Manual elements survive |
| 450 | + $this->assertInstanceOf(ToolReference::class, $this->registry->getTool('manual_tool')); |
| 451 | + $this->assertInstanceOf(ResourceReference::class, $this->registry->getResource('test://manual')); |
| 452 | + $this->assertInstanceOf(PromptReference::class, $this->registry->getPrompt('manual_prompt')); |
| 453 | + $this->assertInstanceOf(ResourceTemplateReference::class, $this->registry->getResourceTemplate('manual://{id}')); |
455 | 454 |
|
456 | | - // Test that all discovered elements throw exceptions |
| 455 | + // Discovered elements are gone |
457 | 456 | $this->expectException(ToolNotFoundException::class); |
458 | 457 | $this->registry->getTool('discovered_tool'); |
| 458 | + } |
| 459 | + |
| 460 | + public function testClearRemovesDiscoveredResources(): void |
| 461 | + { |
| 462 | + $discoveredResource = $this->createValidResource('test://discovered'); |
| 463 | + $this->registry->setDiscoveryState(new DiscoveryState( |
| 464 | + resources: ['test://discovered' => new ResourceReference($discoveredResource, static fn () => 'discovered')], |
| 465 | + )); |
| 466 | + |
| 467 | + $this->registry->clear(); |
459 | 468 |
|
460 | 469 | $this->expectException(ResourceNotFoundException::class); |
461 | 470 | $this->registry->getResource('test://discovered'); |
| 471 | + } |
| 472 | + |
| 473 | + public function testClearRemovesDiscoveredPrompts(): void |
| 474 | + { |
| 475 | + $discoveredPrompt = $this->createValidPrompt('discovered_prompt'); |
| 476 | + $this->registry->setDiscoveryState(new DiscoveryState( |
| 477 | + prompts: ['discovered_prompt' => new PromptReference($discoveredPrompt, static fn () => [])], |
| 478 | + )); |
| 479 | + |
| 480 | + $this->registry->clear(); |
462 | 481 |
|
463 | 482 | $this->expectException(PromptNotFoundException::class); |
464 | 483 | $this->registry->getPrompt('discovered_prompt'); |
| 484 | + } |
| 485 | + |
| 486 | + public function testClearRemovesDiscoveredResourceTemplates(): void |
| 487 | + { |
| 488 | + $discoveredTemplate = $this->createValidResourceTemplate('discovered://{id}'); |
| 489 | + $this->registry->setDiscoveryState(new DiscoveryState( |
| 490 | + resourceTemplates: ['discovered://{id}' => new ResourceTemplateReference($discoveredTemplate, static fn () => 'discovered')], |
| 491 | + )); |
| 492 | + |
| 493 | + $this->registry->clear(); |
465 | 494 |
|
466 | 495 | $this->expectException(ResourceNotFoundException::class); |
467 | 496 | $this->registry->getResourceTemplate('discovered://{id}'); |
@@ -611,6 +640,127 @@ public function testExtractStructuredContentReturnsArrayDirectlyForArrayOutputSc |
611 | 640 | ], $structuredContent); |
612 | 641 | } |
613 | 642 |
|
| 643 | + public function testClearPreservesDynamicallyRegisteredElements(): void |
| 644 | + { |
| 645 | + // 1. Register a manual tool |
| 646 | + $manualTool = $this->createValidTool('manual_tool'); |
| 647 | + $this->registry->registerTool($manualTool, static fn () => 'manual', true); |
| 648 | + |
| 649 | + // 2. Import discovered tools via setDiscoveryState |
| 650 | + $discoveredTool = $this->createValidTool('discovered_tool'); |
| 651 | + $this->registry->setDiscoveryState(new DiscoveryState( |
| 652 | + tools: ['discovered_tool' => new ToolReference($discoveredTool, static fn () => 'discovered')], |
| 653 | + )); |
| 654 | + |
| 655 | + // 3. Register a dynamic tool (not manual, not discovered) |
| 656 | + $dynamicTool = $this->createValidTool('dynamic_tool'); |
| 657 | + $this->registry->registerTool($dynamicTool, static fn () => 'dynamic'); |
| 658 | + |
| 659 | + // 4. Restore discovery state again (simulates next HTTP request) |
| 660 | + $discoveredTool2 = $this->createValidTool('discovered_tool_v2'); |
| 661 | + $this->registry->setDiscoveryState(new DiscoveryState( |
| 662 | + tools: ['discovered_tool_v2' => new ToolReference($discoveredTool2, static fn () => 'discovered_v2')], |
| 663 | + )); |
| 664 | + |
| 665 | + // Manual tool survives |
| 666 | + $this->assertInstanceOf(ToolReference::class, $this->registry->getTool('manual_tool')); |
| 667 | + // Dynamic tool survives |
| 668 | + $this->assertInstanceOf(ToolReference::class, $this->registry->getTool('dynamic_tool')); |
| 669 | + // New discovered tool is present |
| 670 | + $this->assertInstanceOf(ToolReference::class, $this->registry->getTool('discovered_tool_v2')); |
| 671 | + // Old discovered tool is gone |
| 672 | + $this->expectException(ToolNotFoundException::class); |
| 673 | + $this->registry->getTool('discovered_tool'); |
| 674 | + } |
| 675 | + |
| 676 | + public function testGetDiscoveryStateExcludesDynamicTools(): void |
| 677 | + { |
| 678 | + // Import discovered tool via setDiscoveryState |
| 679 | + $discoveredTool = $this->createValidTool('discovered_tool'); |
| 680 | + $this->registry->setDiscoveryState(new DiscoveryState( |
| 681 | + tools: ['discovered_tool' => new ToolReference($discoveredTool, static fn () => 'discovered')], |
| 682 | + )); |
| 683 | + |
| 684 | + // Register a dynamic tool |
| 685 | + $dynamicTool = $this->createValidTool('dynamic_tool'); |
| 686 | + $this->registry->registerTool($dynamicTool, static fn () => 'dynamic'); |
| 687 | + |
| 688 | + // Register a manual tool |
| 689 | + $manualTool = $this->createValidTool('manual_tool'); |
| 690 | + $this->registry->registerTool($manualTool, static fn () => 'manual', true); |
| 691 | + |
| 692 | + $state = $this->registry->getDiscoveryState(); |
| 693 | + |
| 694 | + $this->assertArrayHasKey('discovered_tool', $state->getTools()); |
| 695 | + $this->assertArrayNotHasKey('dynamic_tool', $state->getTools()); |
| 696 | + $this->assertArrayNotHasKey('manual_tool', $state->getTools()); |
| 697 | + } |
| 698 | + |
| 699 | + public function testSetDiscoveryStateRoundTrip(): void |
| 700 | + { |
| 701 | + // Import initial discovered state |
| 702 | + $tool = $this->createValidTool('round_trip_tool'); |
| 703 | + $resource = $this->createValidResource('test://round-trip'); |
| 704 | + $prompt = $this->createValidPrompt('round_trip_prompt'); |
| 705 | + $template = $this->createValidResourceTemplate('round-trip://{id}'); |
| 706 | + |
| 707 | + $initialState = new DiscoveryState( |
| 708 | + tools: ['round_trip_tool' => new ToolReference($tool, static fn () => 'result')], |
| 709 | + resources: ['test://round-trip' => new ResourceReference($resource, static fn () => 'content')], |
| 710 | + prompts: ['round_trip_prompt' => new PromptReference($prompt, static fn () => [])], |
| 711 | + resourceTemplates: ['round-trip://{id}' => new ResourceTemplateReference($template, static fn () => 'tpl')], |
| 712 | + ); |
| 713 | + |
| 714 | + $this->registry->setDiscoveryState($initialState); |
| 715 | + |
| 716 | + // Round-trip: get and set again |
| 717 | + $exportedState = $this->registry->getDiscoveryState(); |
| 718 | + $this->registry->setDiscoveryState($exportedState); |
| 719 | + |
| 720 | + // All elements still present |
| 721 | + $this->assertInstanceOf(ToolReference::class, $this->registry->getTool('round_trip_tool')); |
| 722 | + $this->assertInstanceOf(ResourceReference::class, $this->registry->getResource('test://round-trip')); |
| 723 | + $this->assertInstanceOf(PromptReference::class, $this->registry->getPrompt('round_trip_prompt')); |
| 724 | + $this->assertInstanceOf(ResourceTemplateReference::class, $this->registry->getResourceTemplate('round-trip://{id}')); |
| 725 | + |
| 726 | + // Exported state matches |
| 727 | + $reExportedState = $this->registry->getDiscoveryState(); |
| 728 | + $this->assertCount(\count($exportedState->getTools()), $reExportedState->getTools()); |
| 729 | + $this->assertCount(\count($exportedState->getResources()), $reExportedState->getResources()); |
| 730 | + $this->assertCount(\count($exportedState->getPrompts()), $reExportedState->getPrompts()); |
| 731 | + $this->assertCount(\count($exportedState->getResourceTemplates()), $reExportedState->getResourceTemplates()); |
| 732 | + } |
| 733 | + |
| 734 | + public function testSetDiscoveryStateDoesNotOverwriteManualOrDynamicTools(): void |
| 735 | + { |
| 736 | + // Register a manual tool |
| 737 | + $manualTool = $this->createValidTool('conflict_tool'); |
| 738 | + $this->registry->registerTool($manualTool, static fn () => 'manual_result', true); |
| 739 | + |
| 740 | + // Register a dynamic tool |
| 741 | + $dynamicTool = $this->createValidTool('dynamic_conflict'); |
| 742 | + $this->registry->registerTool($dynamicTool, static fn () => 'dynamic_result'); |
| 743 | + |
| 744 | + // Try to import discovered tools with same names |
| 745 | + $discoveredConflict = $this->createValidTool('conflict_tool'); |
| 746 | + $discoveredDynConflict = $this->createValidTool('dynamic_conflict'); |
| 747 | + $this->registry->setDiscoveryState(new DiscoveryState( |
| 748 | + tools: [ |
| 749 | + 'conflict_tool' => new ToolReference($discoveredConflict, static fn () => 'discovered_result'), |
| 750 | + 'dynamic_conflict' => new ToolReference($discoveredDynConflict, static fn () => 'discovered_dyn_result'), |
| 751 | + ], |
| 752 | + )); |
| 753 | + |
| 754 | + // Manual tool preserved with original handler |
| 755 | + $manualRef = $this->registry->getTool('conflict_tool'); |
| 756 | + $this->assertTrue($manualRef->isManual); |
| 757 | + $this->assertEquals('manual_result', ($manualRef->handler)()); |
| 758 | + |
| 759 | + // Dynamic tool preserved with original handler |
| 760 | + $dynamicRef = $this->registry->getTool('dynamic_conflict'); |
| 761 | + $this->assertEquals('dynamic_result', ($dynamicRef->handler)()); |
| 762 | + } |
| 763 | + |
614 | 764 | private function createValidTool(string $name, ?array $outputSchema = null): Tool |
615 | 765 | { |
616 | 766 | return new Tool( |
|
0 commit comments