ConduitMcp.Client.OAuth.Scopes (ConduitMCP v0.11.0)

Copy Markdown View Source

Scope lists and step-up bookkeeping.

Scopes are lists of strings; on the wire they are one space-delimited string (RFC 6749 §3.3), see parse/1 and join/1.

Step-up (OA-10)

When a server answers 403 with error="insufficient_scope", the client re-authorizes with union/2 of the scopes it requested before and the challenged ones (MCP 2026-07-28 Step-Up Authorization Flow; applied to both eras). To stop a server from looping the client through the authorization server, attempts are counted per {resource, scope set} in a plain map the caller keeps (record_step_up/3) and checked with step_up_allowed?/4 (default limit 2). The scope set is order- and duplicate-insensitive, so ["b", "a"] and ["a", "b", "a"] share a count.

Summary

Types

Step-up attempt counts, keyed by step_up_key/2.

Functions

The scope parameter value for scopes, or nil for an empty list (the parameter is then omitted).

Splits a space-delimited scope string; nil gives []. Empty items from repeated spaces are dropped and duplicates removed, first occurrence kept.

Records one step-up attempt for resource with scopes.

Whether another step-up for resource with scopes is allowed: fewer than max (default 2) attempts are recorded in attempts.

The attempts/0 key for resource and the set of scopes.

previous followed by every scope of challenged not already present, without duplicates: the order of first appearance is kept.

Types

attempts()

@type attempts() :: %{optional({String.t(), [String.t()]}) => non_neg_integer()}

Step-up attempt counts, keyed by step_up_key/2.

Functions

join(scopes)

@spec join([String.t()]) :: String.t() | nil

The scope parameter value for scopes, or nil for an empty list (the parameter is then omitted).

iex> ConduitMcp.Client.OAuth.Scopes.join(["read", "write"])
"read write"

parse(scope)

@spec parse(String.t() | nil) :: [String.t()]

Splits a space-delimited scope string; nil gives []. Empty items from repeated spaces are dropped and duplicates removed, first occurrence kept.

iex> ConduitMcp.Client.OAuth.Scopes.parse("read  write read")
["read", "write"]

record_step_up(attempts, resource, scopes)

@spec record_step_up(attempts(), String.t(), [String.t()]) :: attempts()

Records one step-up attempt for resource with scopes.

step_up_allowed?(attempts, resource, scopes, max \\ 2)

@spec step_up_allowed?(attempts(), String.t(), [String.t()], non_neg_integer()) ::
  boolean()

Whether another step-up for resource with scopes is allowed: fewer than max (default 2) attempts are recorded in attempts.

iex> alias ConduitMcp.Client.OAuth.Scopes
iex> attempts = %{} |> Scopes.record_step_up("https://mcp.example.com", ["a"])
iex> Scopes.step_up_allowed?(attempts, "https://mcp.example.com", ["a"])
true
iex> attempts = Scopes.record_step_up(attempts, "https://mcp.example.com", ["a"])
iex> Scopes.step_up_allowed?(attempts, "https://mcp.example.com", ["a"])
false

step_up_key(resource, scopes)

@spec step_up_key(String.t(), [String.t()]) :: {String.t(), [String.t()]}

The attempts/0 key for resource and the set of scopes.

union(previous, challenged)

@spec union([String.t()], [String.t()]) :: [String.t()]

previous followed by every scope of challenged not already present, without duplicates: the order of first appearance is kept.

iex> ConduitMcp.Client.OAuth.Scopes.union(["read", "write"], ["admin", "read"])
["read", "write", "admin"]