The broker can authorize MCP operations across a stable, domain-neutral resource hierarchy. The model has three independent dimensions:
| Question | Configuration | Example |
|---|---|---|
| Who is calling? | JWT subjects | group:maintenance-area-a |
| What may they do? | Role capabilities | mcp.tools.diagnose |
| Where may they do it? | Resource path assignment | /enterprise/site/area/** |
Authorization is evaluated as:
authorize(subject, capability, resourcePath) -> allow or deny
The broker remains an OAuth 2.1 resource server and enforcement point. It does not issue identities, manage users, or replace the external authorization server.
The authorization engine is domain-neutral. For industrial deployments, the recommended resource hierarchy is aligned with the equipment model from ISA-95 / IEC 62264. This gives policies a recognized industrial vocabulary while keeping the same engine usable in non-manufacturing domains.
The compact paths used in this guide map to ISA-95 concepts as follows:
| Path convention | ISA-95 equipment concept |
|---|---|
enterprise-* |
Enterprise |
site-* |
Site |
area-* |
Area |
line-* |
Production Line, a Work Center |
cell-* |
Work Cell, a Work Unit |
| Final asset segment | Project-specific physical asset extension |
This is an ISA-95-aligned profile, not a claim of full ISA-95 compliance. The broker validates path syntax and authorization semantics, but it does not enforce ISA-95 equipment types or containment rules. UMD-style namespaces can map to the same paths without changing the policy engine.
Resource paths are stable identities separated from slot names, transports, hostnames, and IP addresses:
/enterprise-a/site-paris/area-utilities/line-3/cell-2/compressor-03
In this compact example, line-3 represents a Production Line, cell-2
represents a Work Cell, and compressor-03 is the project-specific physical
asset.
Rules:
/ and are case-sensitive.. and .. are rejected.* in a policy matches exactly one segment.** matches the named resource and every descendant.Examples:
| Pattern | Matches | Does not match |
|---|---|---|
/enterprise/site/area |
That exact area | Its children |
/enterprise/site/* |
Direct children of the site | Grandchildren |
/enterprise/site/** |
The site and every descendant | A sibling site |
Technical provider names may be mapped independently:
{
"auth": {
"slotResources": {
"spoony-00452": "/enterprise-a/site-paris/area-a/line-3/cell-2/motor-7"
}
}
}
Unmapped normal slots resolve to /<slot>. The reserved _broker slot resolves
to /_system/broker.
A role contains functional capabilities only. It never contains a resource path:
{
"auth": {
"roles": {
"viewer": {
"capabilities": [
"mcp.resources.read",
"mcp.tools.list",
"mcp.prompts.read"
]
},
"maintenance": {
"inherits": ["viewer"],
"capabilities": [
"mcp.tools.call",
"mcp.tools.diagnose"
]
},
"administrator": {
"capabilities": ["*"]
}
}
}
}
Inheritance is expanded at startup. Duplicate capabilities are removed.
Undefined inherited roles and inheritance cycles stop startup. * grants every
capability within the resource paths assigned to that role.
The built-in MCP classification is:
| MCP operation | Capability |
|---|---|
resources/list, resources/read |
mcp.resources.read |
tools/list |
mcp.tools.list |
tools/call |
Configured tool capability, otherwise mcp.tools.call |
prompts/list, prompts/get |
mcp.prompts.read |
completion/complete |
mcp.completion.use |
Tool names are never interpreted as capabilities. Configure them explicitly:
{
"auth": {
"toolCapabilities": {
"diagnose_motor": "mcp.tools.diagnose",
"start_motor": "mcp.tools.operate"
},
"providerToolCapabilities": {
"/enterprise-a/site-paris/**": {
"start_motor": "mcp.tools.site-operate"
}
}
}
}
Provider-qualified mappings win over global mappings. The fallback is
mcp.tools.call.
For _broker, catalog and read-only introspection operations use
broker.providers.read. Existing perSlotScopes["_broker"] remains a coarse
OAuth gate evaluated first.
Subjects are derived only from validated JWT claims. Values supplied inside an MCP request are never trusted:
{
"auth": {
"subjectMapping": {
"userClaim": "sub",
"groupClaims": ["groups", "realm.roles"],
"clientClaim": "client_id"
}
}
}
This token:
{
"sub": "alice",
"groups": ["maintenance-area-a", "employees"],
"client_id": "local-assistant"
}
produces:
user:alice
group:maintenance-area-a
group:employees
client:local-assistant
String and string-array claims are accepted. Missing optional claims are
ignored. A configured claim with another shape fails authorization safely.
Duplicate subjects are removed. Subject prefixes are extensible; the initial
model recognizes user:, group:, client:, and service:.
Assignments combine all three dimensions:
{
"auth": {
"assignments": [
{
"id": "maintenance-area-a",
"subject": "group:maintenance-area-a",
"role": "maintenance",
"resource": "/enterprise-a/site-paris/area-a/**"
}
]
}
}
A provider that later connects anywhere below that subtree is governed automatically. No per-provider assignment is needed.
All identities derived for a caller participate in evaluation. An allow from any matching assignment is enough, unless an explicit deny also matches. Absence of a matching allow is a deny.
Denies handle exceptions without fragmenting stable roles:
{
"auth": {
"denies": [
{
"id": "protect-critical-furnace",
"subject": "group:maintenance-area-a",
"capabilities": [
"mcp.tools.configure-analysis",
"mcp.tools.operate"
],
"resource": "/enterprise-a/site-paris/area-a/line-2/cell-4/critical-furnace"
}
]
}
}
An explicit deny always overrides an allow, regardless of declaration order or
whether the deny is exact or inherited through **. A capability of * denies
all operations on matching resources.
Client authorization and provider authentication remain independent. A custom provider authenticator may return a structured principal:
interface IProviderPrincipal {
id: string;
subjects?: readonly string[];
allowedResources?: readonly string[];
metadata?: Readonly<Record<string, unknown>>;
}
For example, a device principal with:
id: device:serial-00452
allowedResources: /enterprise-a/site-paris/area-a/line-3/**
may publish /enterprise-a/site-paris/area-a/line-3/cell-2/motor-7, but cannot
publish a sibling line or a parent site. Dedicated provider connections are
rejected during the WebSocket upgrade. Multiplexed provider connections check
every announced slot separately.
The legacy shared secret returns an unrestricted principal with
allowedResources: ["**"], preserving existing deployments. The abstraction is
compatible with a future mTLS authenticator.
Coarse OAuth scopes run before hierarchical policy checks. Policies apply to direct HTTP, SSE, and WebSocket operations.
For _all:
providerScopes, when present, is also required to pass.Provider names, tool names, and resource paths are not disclosed through a distinct forbidden response.
Denied operations produce structured audit records containing subject ids, client id, slot, resource, capability, provider, tool, decision reason, and matching policy ids. Tokens and secrets are never logged. Allowed decisions are not logged unless:
{
"auth": {
"audit": {
"logAllowed": true
}
}
}
A small installation can use one root hierarchy and two roles:
{
"auth": {
"roles": {
"viewer": {
"capabilities": [
"mcp.resources.read",
"mcp.tools.list",
"mcp.prompts.read"
]
},
"operator": {
"inherits": ["viewer"],
"capabilities": ["mcp.tools.operate"]
}
},
"assignments": [
{
"subject": "group:employees",
"role": "viewer",
"resource": "/enterprise/**"
},
{
"subject": "group:operators",
"role": "operator",
"resource": "/enterprise/**"
}
]
}
}
Keep roles global and stable, then assign them at site or area boundaries:
{
"auth": {
"roles": {
"viewer": {
"capabilities": ["mcp.resources.read", "mcp.tools.list"]
},
"maintenance": {
"inherits": ["viewer"],
"capabilities": [
"mcp.tools.diagnose",
"mcp.tools.configure-analysis"
]
},
"operator": {
"inherits": ["viewer"],
"capabilities": ["mcp.tools.operate"]
}
},
"assignments": [
{
"id": "paris-maintenance",
"subject": "group:maintenance-paris",
"role": "maintenance",
"resource": "/enterprise-a/site-paris/**"
},
{
"id": "line-3-operators",
"subject": "group:operators-line-3",
"role": "operator",
"resource": "/enterprise-a/site-paris/area-a/line-3/**"
},
{
"id": "central-observers",
"subject": "group:central-observers",
"role": "viewer",
"resource": "/enterprise-a/**"
}
]
}
}
providerScopesExisting OAuth configurations remain valid and preserve their prior behavior when no roles, assignments, or denies are configured.
To migrate:
requiredScopes and perSlotScopes as coarse OAuth gates.slotResources.providerScopes entry with one or more subtree assignments._all visibility and direct calls.providerScopes after the equivalent policies are verified.While both models are configured, both restrictions must pass.
providerScopes is deprecated and emits a startup warning.
All policies are compiled, validated, and made immutable at startup. Invalid paths, duplicate ids, unknown roles, role cycles, malformed capabilities, and reserved path misuse stop startup rather than weakening authorization.