feat: hide tool list and instructions from users without LIT MCP access
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:
@@ -120,6 +120,24 @@ wp user create lit-smoke-sub lit-smoke-sub@example.invalid --role=subscriber >/d
|
||||
SUB_TOKEN=$(mint_token lit-smoke-sub)
|
||||
SUB_INIT=$(mcp "$SUB_TOKEN" '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}')
|
||||
SUB_SESSION=$(echo "$SUB_INIT" | grep -i '^mcp-session-id:' | awk '{print $2}' | tr -d '\r' || true)
|
||||
|
||||
# 5a. Visibility (Task 7): VisibilityFilter hides our server's tool list and
|
||||
# instructions from a subscriber entirely, before AbilityRegistrar's own
|
||||
# per-call deny below is even reached. tools/list comes back empty (no direct
|
||||
# tools, no meta-tools either – the adapter still accepts an empty array, see
|
||||
# HandlerHelperTrait::validate_filtered_list); instructions carry only the
|
||||
# no-access notice, never integration names or post types.
|
||||
echo "$SUB_INIT" | grep -q 'nicht freigegeben' \
|
||||
|| fail "subscriber initialize instructions lack the no-access notice: $SUB_INIT"
|
||||
echo "$SUB_INIT" | grep -Eq 'Inhaltstypen|Divi|Pods|Aktive Integrationen|Direkt aufrufbare Tools' \
|
||||
&& fail "subscriber initialize instructions leak integration/post-type details: $SUB_INIT"
|
||||
pass "subscriber initialize instructions carry only the no-access notice"
|
||||
|
||||
SUB_LIST=$(mcp "$SUB_TOKEN" '{"jsonrpc":"2.0","id":10,"method":"tools/list"}' "$SUB_SESSION")
|
||||
echo "$SUB_LIST" | grep -q '"tools":\[\]' \
|
||||
|| fail "subscriber tools/list is not empty: $SUB_LIST"
|
||||
pass "subscriber tools/list is empty (no direct tools, no meta-tools)"
|
||||
|
||||
SUB_CALL=$(mcp "$SUB_TOKEN" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"lit-site-status","arguments":{}}}' "$SUB_SESSION")
|
||||
echo "$SUB_CALL" | grep -Eq '"isError":[[:space:]]*true|"error":[[:space:]]*\{' || fail "subscriber could call lit-site-status: $SUB_CALL"
|
||||
# The denial text must actually be our role-gate message, not just any error
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Server;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Server\VisibilityFilter;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use WP\MCP\Core\McpServer;
|
||||
use WP\McpSchema\Common\Protocol\DTO\InitializeResult;
|
||||
|
||||
final class VisibilityFilterTest extends TestCase
|
||||
{
|
||||
private function server(string $id): McpServer
|
||||
{
|
||||
$server = Mockery::mock(McpServer::class);
|
||||
$server->allows('get_server_id')->andReturn($id);
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
private function initializeResult(): InitializeResult
|
||||
{
|
||||
return InitializeResult::fromArray([
|
||||
'protocolVersion' => '2025-11-25',
|
||||
'capabilities' => ['tools' => ['listChanged' => false]],
|
||||
'serverInfo' => ['name' => 'LIT MCP', 'version' => '0.2.0'],
|
||||
'instructions' => 'Inhaltstypen: page, post, geheim',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUsersWithoutAccessSeeNoToolsOnOurServer(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
$filter = new VisibilityFilter();
|
||||
|
||||
self::assertSame([], $filter->filterTools(['tool-a'], $this->server('mcp-oauth-server')));
|
||||
self::assertSame(['tool-a'], $filter->filterTools(['tool-a'], $this->server('other-server')));
|
||||
}
|
||||
|
||||
public function testAllowedUsersKeepTheToolList(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
|
||||
self::assertSame(['tool-a'], (new VisibilityFilter())->filterTools(['tool-a'], $this->server('mcp-oauth-server')));
|
||||
}
|
||||
|
||||
public function testInstructionsAreReplacedForUsersWithoutAccess(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
|
||||
$result = (new VisibilityFilter())->filterInitialize($this->initializeResult(), $this->server('mcp-oauth-server'));
|
||||
|
||||
self::assertInstanceOf(InitializeResult::class, $result);
|
||||
self::assertSame(VisibilityFilter::NO_ACCESS_INSTRUCTIONS, $result->getInstructions());
|
||||
self::assertSame('2025-11-25', $result->getProtocolVersion());
|
||||
}
|
||||
|
||||
public function testInstructionsStayForAllowedUsersAndOtherServers(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$original = $this->initializeResult();
|
||||
|
||||
self::assertSame($original, (new VisibilityFilter())->filterInitialize($original, $this->server('mcp-oauth-server')));
|
||||
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
self::assertSame($original, (new VisibilityFilter())->filterInitialize($original, $this->server('other-server')));
|
||||
}
|
||||
|
||||
public function testRegisterHooksBothFilters(): void
|
||||
{
|
||||
$filter = new VisibilityFilter();
|
||||
Functions\expect('add_filter')->once()->with('mcp_adapter_tools_list', [$filter, 'filterTools'], 10, 2);
|
||||
Functions\expect('add_filter')->once()->with('mcp_adapter_initialize_response', [$filter, 'filterInitialize'], 10, 2);
|
||||
|
||||
$filter->register();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user