AXIAM PHP SDK

Mcp
in package

FinalYes

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 null when 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 to invalid_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
throws
ValidationError

When any parameter is outside RFC 6750's syntax.

Return values
string

checkGuardAgreement()

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
throws
ValidationError

When the guard's resourceMetadataUrl is not exactly the document's metadataUrl, or its expected audience is not exactly the document's resource.

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
string

pathOf()

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
string

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.

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, or http only on 127.0.0.1, [::1] or localhost. 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
throws
ValidationError

When any CONTRACT.md §28.2 rule is violated.

Return values
ProtectedResourceMetadata

toJsonResponse()

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
Return values
JsonResponse
On this page

Search results