Mcp
in package
MCP resource-server helpers (CONTRACT.md §28, RFC 9728 + RFC 6750) — the ONE §28 implementation both {@see \Axiam\Sdk\Laravel\AxiamMiddleware} and {@see \Axiam\Sdk\Symfony\AxiamAuthSubscriber} are built on, mirroring how this SDK never duplicates a security-critical decision across its two framework bridges (D-02).
§28.0: this SDK implements the RESOURCE SERVER's half and nothing else. AXIAM is the authorization server and implements none of §28; the MCP client's half (parsing a challenge, fetching a document, deciding whether to trust the authorization server it names) is deliberately not in this contract version, for the same reason §20.3 stops at parsing a UMA challenge and goes no further.
No operation here performs network I/O, so §16 (retry) and §9 (single-flight refresh) do not apply and nothing in this class touches the SDK client's own session. Both operations are pure local computation, like UmaChallenge::parse().
Nothing here is a source of truth about a token. The document is a claim a resource server publishes about itself; the challenge is a hint it gives a caller that already failed. Whether a request is authorized stays §10's and §11's decision, unchanged and unreachable from here.
Table of Contents
Constants
- PROTECTED_RESOURCE_METADATA_PREFIX : mixed = '/.well-known/oauth-protected-resource'
- RFC 9728 §3.1's well-known prefix — the segment inserted between a resource's authority and its path to reach the document that describes it.
Methods
- bearerChallenge() : string
- `bearerChallenge(...)` (CONTRACT.md §28.4) — build the **value** of a `WWW-Authenticate` header, never the whole header line and never a map. The caller sets the header.
- checkGuardAgreement() : void
- CONTRACT.md §28.5 rule 3: where this SDK can see BOTH a §10 guard's §28 configuration and the document about to be published for it, it MUST refuse a mismatch at startup rather than publish a document the guard disagrees with.
- deriveMetadataPath() : string
- CONTRACT.md §28.3's derivation: RFC 9728 §3.1 inserts the well-known segment between the authority and the path. An empty path and a bare `/` both reach the root form; anything else is appended, **trailing slash included** — it is part of the identifier a client compares, and two resources that differ only by it are two resources.
- pathOf() : string
- The path component of an already-validated absolute URL — query and fragment stripped, an absent path normalised to `/`. Used by {@see McpGuardOptions::build()} to derive the path a §10 guard exempts from authentication (CONTRACT.md §28.3 rule 2) from the `resourceMetadataUrl` option it was given, without parsing the URL a second time with different rules.
- protectedResourceMetadata() : ProtectedResourceMetadata
- `protectedResourceMetadata(...)` (CONTRACT.md §28.1) — build and validate the RFC 9728 protected-resource metadata document this server publishes about itself, and derive the path and URL it is served at.
- toJsonResponse() : JsonResponse
- The CONTRACT.md §28.3 response for the metadata document, ready to return from a route/controller: `200`, `application/json`, the document body, and the caching / CORS headers §28.3 rules 5–6 name. Shared by {@see \Axiam\Sdk\Laravel\AxiamServiceProvider}'s `Route::serveProtectedResourceMetadata` macro and {@see \Axiam\Sdk\Symfony\ProtectedResourceMetadataController} so the two framework surfaces cannot drift on the response shape.
Constants
PROTECTED_RESOURCE_METADATA_PREFIX
RFC 9728 §3.1's well-known prefix — the segment inserted between a resource's authority and its path to reach the document that describes it.
public
mixed
PROTECTED_RESOURCE_METADATA_PREFIX
= '/.well-known/oauth-protected-resource'
Methods
bearerChallenge()
`bearerChallenge(...)` (CONTRACT.md §28.4) — build the **value** of a `WWW-Authenticate` header, never the whole header line and never a map. The caller sets the header.
public
static bearerChallenge(string $resourceMetadataUrl[, string|null $error = null ][, string|null $errorDescription = null ][, string|null $scope = null ]) : string
Parameters appear in a fixed order — error, error_description, scope,
resource_metadata — separated by exactly , . resource_metadata is always
present; the other three are omitted when not given.
Every value is quoted and no value is ever escaped. RFC 6750 §3 restricts each
parameter to a character set that cannot contain " or \, so a value needing an
escape is a value that does not belong in a challenge: this method refuses it
rather than escaping, truncating or stripping it. A challenge is built from the
code's own constants and a route's own configuration, so an invalid one is a
programming error, not a runtime condition to degrade around.
Parameters
- $resourceMetadataUrl : string
-
The document's URL — the one parameter that is always present. May carry a query and a fragment.
- $error : string|null = null
-
One of BearerChallengeError's three constants, or
nullwhen the request carried no authentication information at all. - $errorDescription : string|null = null
-
A human-readable description, for an application building its own challenge for its own
400. This SDK's own guards never set it — every reason a token is rejected collapses toinvalid_token, indistinguishably (CONTRACT.md §28.4), because every distinction a 401 draws for an unauthenticated stranger is an oracle. - $scope : string|null = null
-
The scope the route asked for, verbatim — one or more tokens joined by a single space.
Tags
Return values
stringcheckGuardAgreement()
CONTRACT.md §28.5 rule 3: where this SDK can see BOTH a §10 guard's §28 configuration and the document about to be published for it, it MUST refuse a mismatch at startup rather than publish a document the guard disagrees with.
public
static checkGuardAgreement(ProtectedResourceMetadata $metadata, McpGuardOptions $guard, string|null $guardExpectedAudience) : void
Called by Route::serveProtectedResourceMetadata() (Laravel,
AxiamServiceProvider) and
ProtectedResourceMetadataController whenever the
integrator passes the guard alongside the document. Passing no guard is how an
integrator whose guard runs in a SEPARATE process opts out — nothing can be
checked there, and nothing is.
Both comparisons are simple string equality (RFC 3986 §6.2.1): no normalisation, no case folding, no trailing-slash tolerance. The two strings are deliberately DIFFERENT ones — the resource and the metadata URL — so each is compared against its own counterpart, never against the other.
Parameters
- $metadata : ProtectedResourceMetadata
- $guard : McpGuardOptions
- $guardExpectedAudience : string|null
Tags
deriveMetadataPath()
CONTRACT.md §28.3's derivation: RFC 9728 §3.1 inserts the well-known segment between the authority and the path. An empty path and a bare `/` both reach the root form; anything else is appended, **trailing slash included** — it is part of the identifier a client compares, and two resources that differ only by it are two resources.
public
static deriveMetadataPath(string $resourcePath) : string
Parameters
- $resourcePath : string
Return values
stringpathOf()
The path component of an already-validated absolute URL — query and fragment stripped, an absent path normalised to `/`. Used by {@see McpGuardOptions::build()} to derive the path a §10 guard exempts from authentication (CONTRACT.md §28.3 rule 2) from the `resourceMetadataUrl` option it was given, without parsing the URL a second time with different rules.
public
static pathOf(string $absoluteUrl) : string
Parameters
- $absoluteUrl : string
Return values
stringprotectedResourceMetadata()
`protectedResourceMetadata(...)` (CONTRACT.md §28.1) — build and validate the RFC 9728 protected-resource metadata document this server publishes about itself, and derive the path and URL it is served at.
public
static protectedResourceMetadata(string $resource, array<int, string> $authorizationServers[, array<int, string> $scopesSupported = [] ][, array<int, string> $bearerMethodsSupported = ['header'] ][, string|null $resourceDocumentation = null ]) : ProtectedResourceMetadata
Validation happens here and it refuses; it never repairs. Every §28.2 rule is checked before any route exists and before any request is served, and a violation throws ValidationError. Nothing is normalised, trimmed, lowercased or re-encoded to make it pass: a value that needs adjusting is a configuration mistake an operator fixes in one line, and a helper that quietly fixed it would publish a document describing a resource server that does not exist.
Nothing in the document may come from a request (§28.2 rule 8). Both
$resource and $authorizationServers are configuration; this method offers no
way to build either from a Host header, the Forwarded/X-Forwarded-* family
or a request URL, because a document assembled from the request is a document an
attacker can point at an authorization server of their choosing — the whole
handshake redirected with one header.
Parameters
- $resource : string
-
Absolute URI with a scheme and an authority, no query and no fragment.
https, orhttponly on127.0.0.1,[::1]orlocalhost. A trailing slash is significant. - $authorizationServers : array<int, string>
-
The issuer identifiers of the authorization servers guarding this resource — at least one, each verbatim, no query, no fragment, no duplicates.
- $scopesSupported : array<int, string> = []
-
The scope tokens this resource server understands. Order is preserved, duplicates are refused, and an empty list omits the member from the document.
- $bearerMethodsSupported : array<int, string> = ['header']
-
Defaults to
['header'], the only accepted value in this contract version. - $resourceDocumentation : string|null = null
-
Optional documentation page for a human. May carry a query and a fragment; omitted from the document when
null.
Tags
Return values
ProtectedResourceMetadatatoJsonResponse()
The CONTRACT.md §28.3 response for the metadata document, ready to return from a route/controller: `200`, `application/json`, the document body, and the caching / CORS headers §28.3 rules 5–6 name. Shared by {@see \Axiam\Sdk\Laravel\AxiamServiceProvider}'s `Route::serveProtectedResourceMetadata` macro and {@see \Axiam\Sdk\Symfony\ProtectedResourceMetadataController} so the two framework surfaces cannot drift on the response shape.
public
static toJsonResponse(ProtectedResourceMetadata $metadata) : JsonResponse
Parameters
- $metadata : ProtectedResourceMetadata