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:
'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 theadmintag disarms adeny: ['tag:admin']rule — exposure stays bounded by theoperationsallow-list, but the deny rule itself is only as trustworthy as the spec source. Prefer name patterns for deny rules over a remote spec; keeptag: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:
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);
}
}'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
ConditionalToolInterfacewhen a capability should never exist on a given deployment at all, regardless of session.