rasuvaeff/yii3-mcp-rbac-bridge

Per-user RBAC for MCP servers: yiisoft/rbac permissions on yii3-mcp tool calls, visibility filtering and session-identity binding

Maintainers

Package info

github.com/rasuvaeff/yii3-mcp-rbac-bridge

pkg:composer/rasuvaeff/yii3-mcp-rbac-bridge

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-07 19:33 UTC

This package is auto-updated.

Last update: 2026-07-07 19:35:49 UTC


README

Stable Version Total Downloads Build Static analysis Psalm level PHP License

Per-user authorization for rasuvaeff/yii3-mcp servers over the Yii3 auth stack — the application-facing alternative to OAuth 2.1: RBAC permissions enforced on every tools/call, permission-aware tools/list filtering, and session-identity binding against session hijacking.

Using an AI coding assistant? llms.txt contains a compact API reference you can share with the model. Contributors: see AGENTS.md.

Requirements

Requirement Version
PHP 8.3 – 8.5
rasuvaeff/yii3-mcp ^1.1
yiisoft/access ^2.0 (bind AccessCheckerInterface to your RBAC manager)
yiisoft/user ^2.0 (identity of the current request)

Installation

composer require rasuvaeff/yii3-mcp-rbac-bridge

The model: two auth layers

SharedSecretMiddleware (yii3-mcp) stays — it is machine auth: may this MCP client talk to this endpoint at all. This bridge adds user auth: what may the authenticated user behind the call actually do. Both layers run; removing the shared secret does not follow from adding RBAC.

// config/routes.php — secret first (cheap fail-closed), then identity
Route::methods(['POST', 'GET', 'DELETE', 'OPTIONS'], '/mcp')
    ->middleware(SharedSecretMiddleware::class)
    ->middleware(Authentication::class)       // yiisoft/auth: token -> CurrentUser
    ->action(McpAction::class),

Usage

1. Declare permissions on tools

use Rasuvaeff\Yii3McpRbacBridge\RequiredPermission;

final readonly class OrderTools
{
    #[McpTool(name: 'order.status')]
    #[RequiredPermission('orders.view')]
    public function status(string $orderId): string { ... }

    #[McpTool(name: 'ping')]          // no attribute = unrestricted
    public function ping(): string { ... }
}

Restriction is explicit and per-tool: tools without a permission stay open (behind the shared secret). #[RequiredPermission] on a method without #[McpTool] fails the build — a permission that would never be enforced is a bug, not a default. One tool name mapped to two different permissions by attributes fails the build too (a silent last-one-wins would enforce an arbitrary one); explicit overrides win by design.

Tool names: what the map keys must be

PermissionMap keys are tool names, and the bridge derives each one exactly as yii3-mcp registers it — so list and call can never key off different names:

Tool declaration Registered name = map key
#[McpTool(name: 'order.status')] order.status — the explicit name wins
#[McpTool] on public function status() status — the method name
#[McpTool] on public function __invoke() the class short name (e.g. RefundTool), not __invoke

fromToolClasses() computes these keys for you. Only an explicit map (the $overrides argument, or new PermissionMap([...])) is yours to key correctly — for an invokable tool that key is the class short name:

new PermissionMap(['RefundTool' => 'orders.refund']);   // invokable RefundTool::__invoke

A key that matches no registered tool is inert (the tool stays unrestricted), so keep explicit keys in sync with the tool names above.

2. Wire the bridge

// config/common/di/mcp-rbac.php
use Rasuvaeff\Yii3McpRbacBridge\ {
    CurrentUserIdentitySource, IdentitySourceInterface, PermissionMap,
};

return [
    IdentitySourceInterface::class => CurrentUserIdentitySource::class,
    PermissionMap::class => static fn () => PermissionMap::fromToolClasses(
        [OrderTools::class],                       // same list as the `tools` params
        // ['order.status' => 'orders.admin'],     // optional explicit overrides
    ),
];
// config/params.php
'rasuvaeff/yii3-mcp' => [
    'tools' => [OrderTools::class],
    'interceptors' => [
        SessionIdentityInterceptor::class,   // outermost: binding before anything trusts the session
        RbacToolCallInterceptor::class,
    ],
    'tool_visibility' => RbacToolVisibility::class,
],

AccessCheckerInterface comes from your RBAC setup (yiisoft/rbac manager with rbac-php/rbac-db storage — see suggest).

What each piece does

Class Role
RbacToolCallInterceptor rejects tools/call without the mapped permission (regular MCP tool error, fail-closed for guests)
RbacToolVisibility hides the same tools from tools/list — list and call can never disagree (one PermissionMap)
SessionIdentityInterceptor binds the MCP session to its first identity; a leaked Mcp-Session-Id presented with another user's token is rejected
PermissionMap tool name → permission: #[RequiredPermission] scan + explicit overrides
CurrentUserIdentitySource identity id from yiisoft/user CurrentUser (null = guest); implement IdentitySourceInterface for stdio/custom setups

Security notes

  • Session binding happens on the first tools/call (the yii3-mcp interceptor chain does not see initialize). Between initialize and the first call the session carries no identity — nothing is authorized in that window, so nothing is exposed; the binding covers every actual operation.
  • Guests are first-class: a guest binds the session as a guest and is denied on every permission-mapped tool (AccessCheckerInterface receives null). The internal guest marker cannot be forged by a literal "guest" user id.
  • Permission revocation applies on the next call (fail-closed). Live notifications/tools/list_changed on revocation is not part of this version — the SDK exposes it only with an event dispatcher, which yii3-mcp's factory does not yet carry.
  • For stdio (mcp:serve) there is no HTTP request: bind IdentitySourceInterface to an env-/config-driven implementation, or leave the shared secret as the only (machine) identity.

Examples

See examples/ — runs offline.

Script Shows Needs server?
rbac.php Filtered listing, allowed/denied calls, session binding no

Development

No PHP/Composer on the host — run in Docker via the composer:2 image:

docker run --rm -v "$PWD":/app -w /app composer:2 composer build

Or with Make: make build, make cs-fix, make psalm, make test.

License

BSD-3-Clause. See LICENSE.md.