feat: register OAuth MCP server with direct tools
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -4,6 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace LIT\MCP;
|
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.
|
* Wires WP-LIT-MCP into WordPress.
|
||||||
*/
|
*/
|
||||||
@@ -22,6 +28,15 @@ final class Plugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
(new Access())->register();
|
(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
|
public static function pluginDir(): string
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Server;
|
||||||
|
|
||||||
|
use LIT\MCP\Guides\InstructionsBuilder;
|
||||||
|
use LIT\MCP\Integrations\IntegrationRegistry;
|
||||||
|
use WP\MCP\Core\McpAdapter;
|
||||||
|
use WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler;
|
||||||
|
use WPMedia\MCP\OAuth\Transport\OAuthHttpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the OAuth-protected MCP server with our own tool list.
|
||||||
|
*
|
||||||
|
* wp-media/mcp-oauth hardcodes server id/route (token audience) and only three
|
||||||
|
* meta-tools. We register the same server id first (priority 5 < mcp-oauth's 10);
|
||||||
|
* mcp-oauth's later attempt fails with "duplicate server id" (logged via
|
||||||
|
* _doing_it_wrong in debug mode only) – see spec §4.2.
|
||||||
|
*/
|
||||||
|
final class ServerRegistrar
|
||||||
|
{
|
||||||
|
public const SERVER_ID = 'mcp-oauth-server';
|
||||||
|
public const NAMESPACE = 'mcp';
|
||||||
|
public const ROUTE = 'mcp-oauth-server';
|
||||||
|
public const META_TOOLS = [
|
||||||
|
'mcp-adapter/discover-abilities',
|
||||||
|
'mcp-adapter/get-ability-info',
|
||||||
|
'mcp-adapter/execute-ability',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var callable(): bool */
|
||||||
|
private $oauthEnabled;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private IntegrationRegistry $registry,
|
||||||
|
private InstructionsBuilder $instructions,
|
||||||
|
?callable $oauthEnabled = null
|
||||||
|
) {
|
||||||
|
$this->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<string, mixed>|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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Server;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Abilities\Ability;
|
||||||
|
use LIT\MCP\Guides\InstructionsBuilder;
|
||||||
|
use LIT\MCP\Integrations\Integration;
|
||||||
|
use LIT\MCP\Integrations\IntegrationRegistry;
|
||||||
|
use LIT\MCP\Server\ServerRegistrar;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use Mockery;
|
||||||
|
|
||||||
|
final class ServerRegistrarTest extends TestCase
|
||||||
|
{
|
||||||
|
private function registry(): IntegrationRegistry
|
||||||
|
{
|
||||||
|
$direct = Mockery::mock(Ability::class);
|
||||||
|
$direct->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user