Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions docs/docs/tutorials/security/security_best_practices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,146 @@ processes:
- Use unix domain sockets or other Interprocess Communication (IPC)
mechanisms with restricted access

### Scoped Authorization Receipts for High-Risk Tool Calls

MCP authorization establishes whether a client can reach an MCP server.
For high-risk tool calls, such as durable state mutation, spending
money, exporting sensitive data, or changing permissions, some
deployments also need to verify that an enterprise gateway or runtime
authorized this exact agent-originated action before the provider runs
business logic.

#### Layered Authorization Model

Scoped authorization receipts can help deployments keep three decisions
separate:

1. **Transport authorization**: OAuth or another authorization mechanism
establishes that the client can access the MCP server.
2. **Runtime action authorization**: a gateway or enterprise runtime
admits this exact tool call for a specific user, agent, resource,
request digest, and policy context.
3. **Provider business authorization**: the provider decides whether to
execute the operation before mutating state.

This is a non-normative pattern. MCP core does not define a receipt
format, require a particular issuer, or replace provider-side business
authorization.

#### Example Receipt Shape

A provider-hosted MCP server can require a scoped receipt in tool
arguments or request metadata for selected high-risk tools. A receipt
profile is deployment-specific, but the fields below are often useful
for replay resistance and auditability:

```json
{
"phase": "admission",
"state": "admitted",
"issuer": "https://gateway.example.com",
"audience": "provider-crm-mcp/provider.crm.update_customer",
"decision_id": "dec_123",
"action_id": "act_456",
"tool": "provider.crm.update_customer",
"action": "write",
"resource": "provider/customer/cus_123",
"request_digest": "sha256:...",
"policy_version": "crm-write-policy-2026-06",
"case_id": "case_1042",
"customer_id": "cus_123",
"approval_id": "approval_789",
"jit_grant_id": "grant_012",
"idempotency_key": "case_1042:update-cus_123:dec_123",
"expires_at": "2026-06-03T18:00:00Z"
}
```

The `request_digest` should be computed over a deployment-defined
canonical representation of the method, tool name, tool arguments, and
relevant policy context. The receipt issuer and verifier should agree
out of band on the exact canonicalization and projection used for this
digest. If the provider cannot reproduce the digest, the request should
fail closed before business logic runs.

Because receipts travel with client-submitted tool calls, the provider
also needs to verify that the receipt was issued by a trusted authority
and has not been modified by the client. This can be done with an issuer
signature or other integrity protection the provider can validate
independently of the client. Only after authenticity is established do
freshness, audience, tool, resource, context, and request-digest checks
carry security weight.

The receipt state machine should remain separate from provider business
logic. Example states include:

- `admitted`: this exact request was accepted for possible execution.
- `completed`: the provider has a prior outcome for the same action or
receipt ID.
- `denied`: policy, user, or provider refused before execution.
- `expired`: the receipt is no longer fresh.
- `out_of_scope`: the request digest, resource, action, or policy
context no longer matches.
- `already_consumed`: the receipt cannot authorize another mutation.

#### Retry Suppression and Scope Drift

Providers that use scoped receipts should explicitly handle replay,
retry suppression, and scope drift:

1. A gateway admits `provider.crm.update_customer` for `case_1042` and
`cus_123`, bound to stable action ID A, canonical request digest D,
policy version P, and expiry T.
2. The client submits the tool call with the scoped receipt.
3. The provider completes or reserves the mutation, but transport times
out before the agent loop records the outcome.
4. The client retries with the same receipt, action ID, and request
digest. The provider returns the prior outcome, completed receipt, or
an `already_consumed` terminal state without running the mutating
handler again.
5. The client changes the resource, customer, action, request digest,
policy version, action ID, or context. The provider rejects the
receipt as `out_of_scope` or stale before business logic runs.
6. The client reuses the same receipt for a second mutation. The
provider rejects it as `already_consumed`.

The key acceptance criterion is that retry after timeout does not cause
duplicate mutation. A provider idempotency key can be linked to the
stable action or receipt ID, but should not be the only
verifier-readable boundary.

#### Signed Record Examples

SEP-2828 is one possible signed-record pattern for this guidance, not a
required MCP receipt schema. The useful compatibility target is the set
of verifier obligations: bind the decision and outcome to a canonical
request, bind them to the same call instance, link the outcome back to
the decision, and check issuer, audience, scope, freshness, and
integrity when the verifier has the issuer's key. Runtime truth, issuer
trust policy, and single-use or `already_consumed` state remain outside
the signed record.

Fixture suites for signed-record profiles should include positive
decision/outcome pairs, substituted decision or outcome negatives,
replay and scope-drift cases, and equal-timestamp ordering cases. If two
decision records for the same call binding have the same decision time
and no explicit sequence or revision field, the verifier should treat
the result as ambiguous or invalid instead of choosing a winner from file
order, arrival order, or alphabetical ordering.

If a deployment supports a fallback path without a SEP-2787 attestation,
the decision back-link should name the fallback projection version that
produced the digest, for example
`tools_call_params_plus_meta_authorization_binding_v1`. That projection
should be an inclusion rule: include the canonical `tools/call` params
that are meant to bind to the decision, include only a named binding
block such as `_meta.authorization_binding`, and exclude all other
`_meta` fields by construction. Transport-local or observation-local
metadata, such as progress tokens, trace context, UI hints, or unrelated
SEP blocks, should not affect the digest. Missing, unknown, or malformed
projection identifiers or binding blocks should fail closed rather than
falling back to hashing the whole observed `_meta`.

### Scope Minimization

Poor scope design increases token compromise impact, elevates user
Expand Down
Loading