Noizu.MCP.Auth.Resource (Noizu MCP v0.1.6)

Copy Markdown View Source

Canonical resource identifiers (RFC 8707 / RFC 9728).

An MCP mount's resource URI is the audience its access tokens are bound to. Getting the comparison wrong is the whole confused-deputy problem: a token minted for https://host/mcp must not open https://host/mcp/learning. So the contract here is byte-exact matching, and normalization is limited to the handful of transformations the RFCs declare scheme-insignificant:

  • scheme lowercased, and restricted to http/https
  • host lowercased (DNS is case-insensitive)
  • the default port for the scheme dropped

Everything else is left alone. In particular there is no trailing-slash coercion…/mcp and …/mcp/ are different resources, because a client that asked for one must not be handed the other, and because the protected resource metadata document has to byte-match whatever the user typed.

Fragments are rejected (RFC 8707 §2 forbids them); a fragment would let two distinct strings name one resource.

iex> Noizu.MCP.Auth.Resource.normalize("HTTPS://App.Example.COM:443/mcp/learning")
{:ok, "https://app.example.com/mcp/learning"}

iex> Noizu.MCP.Auth.Resource.equal?("https://x/mcp", "https://x/mcp/")
false

Summary

Functions

Build a mount's canonical resource from an issuer origin and a path.

True when both sides normalize to the same canonical URI.

True when resource equals the single configured resource, or is a member of the configured list.

Normalize a resource URI, or {:error, :invalid_resource} when it is not a usable absolute identifier.

Normalize or raise. For mount configuration, where a bad resource URI is a deployment error that should surface at boot rather than as a silent 401.

Types

error()

@type error() :: :invalid_resource

t()

@type t() :: String.t()

Functions

build(issuer, path)

@spec build(t(), String.t() | nil) :: {:ok, t()} | {:error, error()}

Build a mount's canonical resource from an issuer origin and a path.

The issuer is an origin with no path (the design decision that collapses the RFC 8414 path-insertion ambiguity), so this is a plain concatenation — the path is not rewritten, only required to be absolute.

iex> Noizu.MCP.Auth.Resource.build("https://app.example.com", "/mcp/learning")
{:ok, "https://app.example.com/mcp/learning"}

equal?(a, b)

@spec equal?(term(), term()) :: boolean()

True when both sides normalize to the same canonical URI.

matches?(resource, allowed)

@spec matches?(term(), t() | [t()]) :: boolean()

True when resource equals the single configured resource, or is a member of the configured list.

A list is supported for issuer/mount migrations. It never widens what a token reaches — each entry still has to match byte-exactly.

normalize(resource)

@spec normalize(term()) :: {:ok, t()} | {:error, error()}

Normalize a resource URI, or {:error, :invalid_resource} when it is not a usable absolute identifier.

normalize!(resource)

@spec normalize!(term()) :: t()

Normalize or raise. For mount configuration, where a bad resource URI is a deployment error that should surface at boot rather than as a silent 401.