Skip to content

MCP slice 7: [MirrorsRoute] parity guard — tool RBAC cannot drift from its route #810

Closed

Description

@mforce

Slice 7 of #789. Blocked on slices 3-6 (needs tools to check). Can land alongside the last of them.

This guard catches RBAC drift from outside the MCP code.

The risk it closes

MCP tools call the Application layer directly, so they inherit none of a route's .RequireAuthorization(...). Tool RBAC is therefore declared twice, once on the tool and once on the route. The two can silently diverge when someone tightens a route and never opens Mcp/.

The mechanism

Each tool declares [MirrorsRoute("ListSalesOrders", "GetSalesOrder")] or [MirrorsNoRoute(reason)]. The guard walks every tool and resolves each declared name from the live EndpointDataSource. It derives each route's admitted EffectiveAccountRole set by running the real policies against one synthetic principal per role. It then asserts the tool's set is a subset of the intersection.

Set containment, not policy-name equality. The five policies are not a total order. ProductionWrite = {Owner, Manager, Worker} and SalesAccess = {Owner, Manager, Sales}. Neither contains the other, so comparing names would pass a genuine widening.

Keyed by WithName(), what the code says about itself, never by URL or file:line (#632). The guard must also fail a declared name that no longer resolves. That half keeps the registry honest.

The mutation that proves it

Tighten /api/v1/customers to AdminOnly in Program.cs and change nothing under Mcp/. The guard must go red. That is the whole point: drift detection firing from outside the code it protects.

Replaces a weaker approach

This supersedes hand-listing expected tool sets per role. That list is a remembered one, and AGENTS.md says a registry guard exists to stop anyone trusting such lists.

Design

docs/plans/770-mcp-server/02-guards.md rows 13-16.

Activity

  1. added
    sliceThin vertical work item
    area:apiAPI/endpoint layer
    priority:tier3Real product weight, real cost
    size:MA day or two; migration or a multi-state UI
    epic-789MCP server support (#789)
    on Sep 13, 2026
  2. added this to the MCP server support milestone on Sep 13, 2026
  3. mforce commented on Oct 10, 2026

    @mforce
    OwnerAuthor

    Re-check after milestone 10, 2026-10-10. Role parity stays as written: REST routes keep their role policies. Scope has nothing to mirror, because no REST route declares one. Add a separate scope walk:

    • every tool carries exactly one scope policy;
    • a tool with ReadOnly == false requires daily-entries:write, and every other tool requires farm:read;
    • every value in OAuthScopes.All is used by at least one tool.

    Pair it with a causal test: a token holding only farm:read sees no write tool in tools/list and is refused if it calls one.

  4. mforce commented on Oct 11, 2026

    @mforce
    OwnerAuthor

    Amendment, 2026-10-11. #1209 (#807) shipped most of this slice:

    • [MirrorsRoute] (src/Cluckwork.Api/Mcp/MirrorsRouteAttribute.cs) and McpRouteParityTests. The test walks every tool, resolves the route by WithName() from the live EndpointDataSource, and runs both sides' policies through the live authorization service against one principal per role. A tool must admit no role its route denies. A missing attribute and a name that no longer resolves are both findings.

    Still open, and folded into #808 by the maintainer's decision:

    The PR for #808 will close this issue too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiAPI/endpoint layerepic-789MCP server support (#789)priority:tier3Real product weight, real costsize:MA day or two; migration or a multi-state UIsliceThin vertical work item

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions