diff --git a/src/Plugin.php b/src/Plugin.php index db2ec0e..b3b19ca 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -4,6 +4,12 @@ declare(strict_types=1); namespace LIT\MCP; +use LIT\MCP\Abilities\AbilityRegistrar; +use LIT\MCP\Guides\InstructionsBuilder; +use LIT\MCP\Integrations\Core\CoreIntegration; +use LIT\MCP\Integrations\IntegrationRegistry; +use LIT\MCP\Server\ServerRegistrar; + /** * Wires WP-LIT-MCP into WordPress. */ @@ -22,6 +28,15 @@ final class Plugin } (new Access())->register(); + + $registry = new IntegrationRegistry( + new CoreIntegration(self::pluginDir() . '/docs/guides') + ); + + (new AbilityRegistrar($registry))->register(); + (new ServerRegistrar($registry, new InstructionsBuilder($registry)))->register(); + + \WPMedia\MCP\OAuth\Bootstrap::instance(); } public static function pluginDir(): string diff --git a/src/Server/ServerRegistrar.php b/src/Server/ServerRegistrar.php new file mode 100644 index 0000000..9cbb326 --- /dev/null +++ b/src/Server/ServerRegistrar.php @@ -0,0 +1,100 @@ +oauthEnabled = $oauthEnabled + ?? static fn (): bool => (bool) apply_filters('wpmedia_mcp_oauth_server_enabled', true); + } + + public function register(): void + { + add_action('mcp_adapter_init', [$this, 'createServer'], 5); + } + + /** @return array|null */ + public function serverArguments(): ?array + { + if (! ($this->oauthEnabled)()) { + return null; + } + + return [ + 'id' => self::SERVER_ID, + 'namespace' => self::NAMESPACE, + 'route' => self::ROUTE, + 'name' => 'LIT MCP', + 'description' => $this->instructions->build(), + 'version' => '0.1.0', + 'tools' => array_merge($this->registry->directAbilityNames(), self::META_TOOLS), + ]; + } + + public function createServer(): void + { + if ( + ! doing_action('mcp_adapter_init') + || ! class_exists(McpAdapter::class) + || ! class_exists(OAuthHttpTransport::class) + ) { + return; + } + + $args = $this->serverArguments(); + if ($args === null) { + return; + } + + $result = McpAdapter::instance()->create_server( + $args['id'], + $args['namespace'], + $args['route'], + $args['name'], + $args['description'], + $args['version'], + [OAuthHttpTransport::class], + ErrorLogMcpErrorHandler::class, + null, + $args['tools'] + ); + + if (is_wp_error($result)) { + $message = '[wp-lit-mcp] MCP server registration failed: ' . $result->get_error_message(); + error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + } + } +} diff --git a/tests/Unit/Server/ServerRegistrarTest.php b/tests/Unit/Server/ServerRegistrarTest.php new file mode 100644 index 0000000..bc25cda --- /dev/null +++ b/tests/Unit/Server/ServerRegistrarTest.php @@ -0,0 +1,80 @@ +allows('name')->andReturn('lit/guide'); + $direct->allows('isDirect')->andReturn(true); + $indirect = Mockery::mock(Ability::class); + $indirect->allows('name')->andReturn('lit/rare-tool'); + $indirect->allows('isDirect')->andReturn(false); + + $core = Mockery::mock(Integration::class); + $core->allows('id')->andReturn('core'); + $core->allows('isAvailable')->andReturn(true); + $core->allows('abilities')->andReturn([$direct, $indirect]); + + return new IntegrationRegistry($core); + } + + private function instructions(): InstructionsBuilder + { + // InstructionsBuilder is final (not mockable) and pure – use the real one. + return new InstructionsBuilder($this->registry()); + } + + public function testServerUsesOauthRouteDirectToolsPlusMetaToolsAndInstructions(): void + { + $args = (new ServerRegistrar($this->registry(), $this->instructions(), static fn () => true))->serverArguments(); + + self::assertSame('mcp-oauth-server', $args['id']); + self::assertSame('mcp', $args['namespace']); + self::assertSame('mcp-oauth-server', $args['route']); + self::assertStringContainsString('Aktive Integrationen: core', $args['description']); + self::assertSame( + ['lit/guide', 'mcp-adapter/discover-abilities', 'mcp-adapter/get-ability-info', 'mcp-adapter/execute-ability'], + $args['tools'] + ); + self::assertNotContains('lit/rare-tool', $args['tools']); + } + + public function testNoServerWhenOauthIsDisabled(): void + { + $registrar = new ServerRegistrar($this->registry(), $this->instructions(), static fn () => false); + + self::assertNull($registrar->serverArguments()); + } + + public function testCreateServerOutsideAdapterInitDoesNothing(): void + { + Functions\when('doing_action')->justReturn(false); + $oauthChecked = false; + $registrar = new ServerRegistrar( + $this->registry(), + $this->instructions(), + static function () use (&$oauthChecked): bool { + $oauthChecked = true; + return true; + } + ); + + $registrar->createServer(); + + self::assertFalse($oauthChecked, 'createServer must bail out before touching the adapter'); + } +}