once() ->with(Access::OPTION, ['administrator']) ->andReturn(['administrator']); self::assertSame(['administrator'], (new Access())->allowedRoles()); } public function testRoleListIsFilterable(): void { Functions\when('get_option')->justReturn(['administrator']); Filters\expectApplied('lit_mcp_allowed_roles') ->once() ->andReturn(['administrator', 'editor']); self::assertSame(['administrator', 'editor'], (new Access())->allowedRoles()); } public function testCorruptFilteredResultFallsBackToAdministrator(): void { Functions\when('get_option')->justReturn(['administrator']); Filters\expectApplied('lit_mcp_allowed_roles') ->once() ->andReturn('editor'); self::assertSame(['administrator'], (new Access())->allowedRoles()); } public function testGrantsCapabilityToAllowedRole(): void { Functions\when('get_option')->justReturn(['editor']); $caps = (new Access())->grantCapability( [], [Access::CAPABILITY], [], new WP_User(5, ['editor']) ); self::assertTrue($caps[Access::CAPABILITY]); } public function testDoesNotGrantCapabilityToOtherRoles(): void { Functions\when('get_option')->justReturn(['administrator']); $caps = (new Access())->grantCapability( [], [Access::CAPABILITY], [], new WP_User(7, ['subscriber']) ); self::assertArrayNotHasKey(Access::CAPABILITY, $caps); } public function testCorruptOptionFallsBackToAdministrator(): void { Functions\when('get_option')->justReturn('not-an-array'); self::assertSame(['administrator'], (new Access())->allowedRoles()); } public function testRegisterRoutesAdapterMetaToolCapabilitiesToOwnCapability(): void { // Brain Monkey records add_filter() but never runs callbacks, so we // capture each registered callback and invoke it ourselves. foreach (['discover_abilities', 'get_ability_info', 'execute_ability'] as $tool) { Filters\expectAdded("mcp_adapter_{$tool}_capability") ->once() ->whenHappen(static function (callable $callback): void { self::assertSame(Access::CAPABILITY, $callback('read')); }); } $access = new Access(); $access->register(); self::assertNotFalse(has_filter('user_has_cap', [$access, 'grantCapability'])); } protected function tearDown(): void { RequestContext::reset(); parent::tearDown(); } /** @return array The args of a meta tool as the adapter registers them (permission always granted). */ private function metaToolArgs(): array { return ['permission_callback' => static fn (mixed $input = null): bool => true]; } private function asOAuthServerRequest(): void { RequestContext::recordRoute(null, [], new WP_REST_Request('/mcp/mcp-oauth-server')); } public function testRegistersTheFilterThatLimitsTheAdapterMetaTools(): void { $access = new Access(); $access->register(); self::assertNotFalse(has_filter('wp_register_ability_args', [$access, 'limitMetaToolsToOwnAbilities'])); } /** Security audit 2026-11, B3: execute-ability and get-ability-info may only name lit/ abilities. */ public function testMetaToolsRefuseForeignAbilitiesOnTheOAuthServer(): void { $this->asOAuthServerRequest(); foreach (['mcp-adapter/execute-ability', 'mcp-adapter/get-ability-info'] as $meta) { $permission = (new Access())->limitMetaToolsToOwnAbilities($this->metaToolArgs(), $meta)['permission_callback']; self::assertTrue($permission(['ability_name' => 'lit/guide']), $meta); foreach (['akismet/comment-check', 'elementor/delete-page', 'litx/guide', '', 'LIT/guide'] as $foreign) { $result = $permission(['ability_name' => $foreign]); self::assertInstanceOf(\WP_Error::class, $result, $meta . ' ' . $foreign); self::assertSame('lit_forbidden', $result->get_error_code()); } self::assertInstanceOf(\WP_Error::class, $permission(null), 'no ability_name'); self::assertInstanceOf(\WP_Error::class, $permission(['ability_name' => ['lit/guide']]), 'not a string'); } } public function testMetaToolsKeepTheirOwnCheckOutsideTheOAuthServer(): void { $permission = (new Access()) ->limitMetaToolsToOwnAbilities($this->metaToolArgs(), 'mcp-adapter/execute-ability')['permission_callback']; self::assertTrue($permission(['ability_name' => 'akismet/comment-check'])); } public function testOtherAbilitiesAreLeftAlone(): void { $args = $this->metaToolArgs(); self::assertSame($args, (new Access())->limitMetaToolsToOwnAbilities($args, 'lit/guide')); self::assertSame($args, (new Access())->limitMetaToolsToOwnAbilities($args, 'mcp-adapter/discover-abilities')); } public function testAMetaToolWithoutAPermissionCallbackFailsClosed(): void { $this->asOAuthServerRequest(); $permission = (new Access())->limitMetaToolsToOwnAbilities([], 'mcp-adapter/execute-ability')['permission_callback']; self::assertFalse($permission(['ability_name' => 'lit/guide'])); } }