Skip to content

PATTERN Cited by 1 source

Per-tool authorization decorator

Pattern

Per-tool authorization decorator is the pattern of enforcing fine-grained, per-operation authorization in-process inside an MCP server via a small decorator (or equivalent declarative annotation) that sits on each tool handler and consults request-context identity claims before executing the body.

Canonical wiki statement (Pinterest, 2026-03-19):

"Inside the server, tools use a lightweight @authorize_tool(policy='…') decorator to enforce finer-grained rules (for example, only Ads-eng groups can call a get_revenue_metrics, even if the server itself is reachable from other orgs)." (Source: sources/2026-03-19-pinterest-building-an-mcp-ecosystem-at-pinterest)

Shape

# Sketch — Pinterest doesn't publish actual code.

@authorize_tool(policy="groups:ads-eng")
def get_revenue_metrics(args):
    # Only reached if the request-context user is in ads-eng.
    return query_presto(args.sql)

@authorize_tool(policy="any-authenticated")
def list_tables(args):
    # Broader allowlist for a less sensitive tool.
    return list_tables_from_catalog()

The decorator:

  1. Reads the request-context identity (populated by upstream JWT validation at Envoy via X-Forwarded-User / X-Forwarded-Groups headers).
  2. Evaluates the policy expression against the identity claims.
  3. On failure: returns an authorization-error MCP result without running the tool body.
  4. On success: invokes the decorated function normally.

Why in-process per-tool — mesh-level alone is too coarse

The mesh layer (patterns/layered-jwt-plus-mesh-auth) decides "can this user reach this server?" That's too coarse for Pinterest's Presto case — the Presto MCP server is reachable from the AI chat surface (broad population), but the get_revenue_metrics tool should only be callable by ads-eng. Options:

  • Split into two servers. One for broad tools, one for ads-eng-only tools. Duplicates infrastructure; creates registry clutter; forces agent planners to know which server has which tool.
  • Let the server reject unauthorized calls internally. Same server, per-tool policy. Pinterest's choice.

The decorator pattern makes the per-tool policy visible (right next to the function body), testable (unit-tested in isolation), and uniform (same decorator across tools).

Two altitudes of authorization compose

  • Coarse (transport / mesh): Envoy validates JWT + enforces server-reachability rules. "Can user reach this server?"
  • Session (optional): Business-group gating at session establishment narrows who can open a session.
  • Fine (in-process): @authorize_tool(...) enforces per-tool rules. "Can user call this specific tool?"

All three altitudes run on the same JWT claims; none is sufficient alone for the range of policies a production MCP ecosystem needs.

Adjacent patterns

  • patterns/jwt-tenant-claim-extraction — sibling pattern at the data-access altitude: never trust a tenant ID from the request body; always read it from the validated JWT claim. Same request-context-identity-as-source-of-truth discipline applied to tenancy.
  • patterns/lambda-authorizer — AWS API Gateway's shape: a separate Lambda runs before the target. Pinterest's decorator is the in-process variant (same JVM / process / runtime), trading the Lambda's isolation for zero additional network hop.
  • concepts/fine-grained-authorization — the generic concept this pattern is one instance of.

Policy-language choice is a degree of freedom

The post does not disclose Pinterest's policy language — policy='…' is opaque. Plausible choices:

  • DSL string"groups:ads-eng" / "roles:admin and region:us".
  • Embedded expression — Python-callable or Python-expression string.
  • External policy engine — OPA / Cedar / Casbin pulled in at decorator time.

Each has trade-offs; Pinterest names none. The shape of the pattern (decorator on each tool, policy string or equivalent) transfers regardless of the policy-language choice.

Seen in

Last updated · 319 distilled / 1,201 read