Skip to content

Visibility

ConditionalToolInterface (see Capabilities) gates registration globally, once, at build time. Visibility is the per-session filter on top of that: which of the registered tools, prompts, and resources a given session may see and call.

Declarative tool visibility

The typical case — no code, just name patterns:

php
'rasuvaeff/yii3-mcp' => [
    'visibility' => [
        'deny' => ['admin.*'],   // hide matches
        'allow' => [],           // non-empty = hide everything it does not match
    ],
],

* matches any run of characters. A tag: prefix matches the tool's tags instead of its name — the OpenAPI bridge propagates OpenAPI tags into the tool's _meta, so 'deny' => ['tag:admin'] hides every bridged operation tagged admin regardless of its served name. A tool with no tags never matches a tag: pattern. Deny wins over allow; both lists empty (the default) means every tool is visible.

Trust boundary on tag: deny rules. Tags come from the OpenAPI document. Over a URL spec, a document that drops the admin tag disarms a deny: ['tag:admin'] rule — exposure stays bounded by the operations allow-list, but the deny rule itself is only as trustworthy as the spec source. Prefer name patterns for deny rules over a remote spec; keep tag: for allow-listing and for local spec files.

Per-session visibility

When the decision depends on the session (admin vs. public client, tenant plan), implement Visibility\ToolVisibilityInterface instead — it runs per session, against the handshake data:

php
use Mcp\Schema\Tool;
use Mcp\Server\Session\SessionInterface;
use Rasuvaeff\Yii3Mcp\Visibility\ToolVisibilityInterface;

final readonly class PlanBasedVisibility implements ToolVisibilityInterface
{
    public function isVisible(Tool $tool, ?SessionInterface $session): bool
    {
        // decide from $session->get('client_info'), tenant data, …
        return !str_starts_with($tool->name, 'admin.') || $this->isAdmin($session);
    }
}
php
'rasuvaeff/yii3-mcp' => [
    'tool_visibility' => PlanBasedVisibility::class,   // DI-resolved
],

Declarative visibility and a tool_visibility class are mutually exclusive — configuring both is a build-time error (LogicException).

Either kind applies in two places, consistently: tools/list omits invisible tools, and tools/call fail-closed rejects them — a client that guesses a hidden name still gets a tool error, and the call never reaches the interceptor chain or the tool itself. This is an early filter, not a replacement for application-level ACL — see the RBAC bridge for authorization that also runs inside the chain, per user.

Prompts and resources

The same seam exists for the other two capabilities, with their own interfaces so a tool policy never accidentally applies to a prompt: Visibility\PromptVisibilityInterface (prompt_visibility) and Visibility\ResourceVisibilityInterface (resource_visibility, covers both static resources and templates). Hiding either reports the capability as not found — indistinguishable from a missing one — for both the list method and a direct prompts/get / resources/read, so listing and fetching can never disagree.

Completions obey the same filters

completion/complete (argument autocompletion, see Capabilities) is served by the SDK straight off the registry — bypassing the reference handler entirely, and with it every interceptor chain. Before Visibility\FilteredCompletionCompleteHandler existed, a prompt or resource hidden by visibility still answered completions for its arguments — verified end-to-end, not inferred — leaking both the suggested values and the capability's existence (a hidden ref answered, a missing one errored). The decorator wraps the SDK's own CompletionCompleteHandler and reports a hidden ref exactly like a missing one, using the SDK's own PromptNotFoundException / ResourceNotFoundException message so the two are byte-identical. It is installed automatically whenever prompt_visibility or resource_visibility is configured — no separate params key.

What visibility is not

Visibility is a per-session filter, evaluated against handshake/session data such as client_info. It is not:

  • an authorization decision tied to an authenticated application user (that belongs to the RBAC bridge, or your own interceptor);
  • an audit trail (the audit-log bridge records what actually ran, independent of what was hidden);
  • a substitute for ConditionalToolInterface when a capability should never exist on a given deployment at all, regardless of session.