Documentation
¶
Overview ¶
Package mcpfacade composes AppTheory's Go runtime MCP OAuth facade.
The package registers the route families defined by runtime/mcproutes.ContractVersion. It serves RFC 9728 protected-resource metadata and RFC 8414 authorization-server discovery while leaving authorization and token behavior in application-owned handlers.
MCP transport routes (POST, GET, and DELETE on every MCP pattern) are registered with an authenticated posture by default: requests without a verified principal are rejected with HTTP 401 and the MCP handler never runs. Set FacadeConfig.TransportPosture to select another posture: runtime-bearer pairs with FacadeConfig.RequireBearerToken to validate an RFC 9728 bearer token in the runtime, and none-public is the explicit public posture.
Mounting on a closed-by-default SecureApp ¶
RegisterMCPFacade installs the facade on the legacy App surface. RegisterMCPFacadeSecure installs the same route inventory, with the same request-time behavior, on a closed-by-default SecureApp. Use the SecureApp registrar for an application that already runs its non-MCP route families on a SecureApp: mounting the facade on the legacy surface instead would drop the application's SecurePrincipalResolver, tier, and PolicyHook for every route that shares the app.
SecureApp requires every route to declare exactly one auth posture at registration, so the SecureApp registrar declares the posture each facade route has always had. The table below is the authority for each family; it is the same table on both surfaces, restated in SecureApp's vocabulary.
Route family SecureApp posture Authority MCP transport, posture iam or jwt Authenticated() the app's SecurePrincipalResolver (the gateway has already validated the caller) MCP transport, posture runtime-bearer Public() the facade's RFC 9728 bearer validator, which fails closed MCP transport, posture none-public Public() none, by explicit facade contract protected-resource metadata Public() none: RFC 9728 metadata is public by design authorization-server discovery Public() none: RFC 8414 metadata is public by design /authorize and /token Public() the client-facing protocol surface itself: PKCE and the single-use code /register (RFC 7591) Public() the DCR handler, which admits public clients only upstream callback Public() the callback handler, which pins state and iss before consuming anything root authorization server Public() none: RFC 8414 metadata is public by design
"Public" here is a declaration, never an omission: a route this package does not register is not reachable at all, and the application's own resolver, tier, and PolicyHook keep governing every route the facade does not own. The PolicyHook (tier P2) runs before the posture gate on both surfaces, so a consumer that denies a request in its hook denies it on facade routes too; that is the consumer's declared authority, not a framework loophole.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Capabilities ¶
type Capabilities struct {
ResponseTypes []string
GrantTypes []string
TokenEndpointAuthMethods []string
CodeChallengeMethods []string
}
Capabilities controls the RFC 8414 capability lists advertised by the facade. Nil fields use DefaultCapabilities; non-nil fields replace a default.
func DefaultCapabilities ¶
func DefaultCapabilities() Capabilities
DefaultCapabilities returns the golden-path OAuth capabilities: code responses, authorization-code and refresh-token grants, public clients, and S256 PKCE.
type FacadeConfig ¶
type FacadeConfig struct {
// IssuerURL is the absolute HTTPS authorization-server issuer advertised by
// both metadata document families when IssuerMode is family.
IssuerURL string
// JWKSURI is the absolute HTTPS key-set URL advertised by both metadata
// document families. In route issuer mode it remains the upstream JWKS,
// because the upstream signs the access tokens the route hands to clients.
JWKSURI string
// RegistrationEndpointURL optionally overrides the RFC 8414 registration
// endpoint. When empty, issuer + /register is advertised (family mode).
RegistrationEndpointURL string
// IssuerMode selects per-route issuer/resource derivation. An empty value
// means IssuerModeFamily: one FacadeConfig.IssuerURL for the whole facade.
// IssuerModeRoute makes each route its own authorization server: the route
// URL is the issuer, the resource, and the advertised authorization server.
IssuerMode mcproutes.IssuerMode
URLMode URLMode
PublicBaseURL string
// UpstreamCallbackPath optionally overrides the stage-global upstream
// callback path. When empty, mcproutes.DefaultCallbackPath is used.
UpstreamCallbackPath string
// AllowedHostnames is required in request-host mode. Configuration entries
// normalize case, trailing dots, and default ports scheme-agnostically;
// request authorities normalize case and trailing dots too, but strip only
// the request scheme's own default port.
// The normalized request authority must exact-match an entry or metadata
// fails with HTTP 400.
AllowedHostnames []string
// Scopes must contain a non-empty scope set for every contract endpoint
// kind. Scope policy remains application-owned.
Scopes map[mcproutes.EndpointKind][]string
// Capabilities selectively overrides DefaultCapabilities.
Capabilities Capabilities
// MCPHandler serves every POST, GET, and DELETE MCP endpoint pattern.
MCPHandler apptheory.Handler
// AuthorizeHandler and TokenHandler are an all-or-none pair. AppTheory only
// mounts the derived paths; all authorization behavior remains app-owned.
AuthorizeHandler HandlerFactory
TokenHandler HandlerFactory
// RegisterHandler mounts the derived RFC 7591 dynamic-client registration
// path per kind. AppTheory only mounts the derived path; registration
// behavior remains app-owned. Absent mounts no register route.
RegisterHandler HandlerFactory
// CallbackHandler mounts the single stage-global upstream OAuth callback
// path. AppTheory only mounts the route; the callback mechanics remain
// app-owned. Absent mounts no callback route.
CallbackHandler apptheory.Handler
// RootAuthorizationServer optionally installs one static GET at the
// algebra-derived root authorization-server discovery path.
RootAuthorizationServer *RootDiscoveryConfig
// TransportPosture selects how the MCP transport routes (POST, GET, and
// DELETE on every MCP pattern) are authorized. An empty value means
// TransportPostureIAM.
//
// TransportPostureRuntimeBearer validates an RFC 9728 bearer token in the
// runtime and requires RequireBearerToken; registration fails closed without
// it. TransportPostureNonePublic registers the transport routes without
// authentication and is never a default. The posture must agree with the CDK
// construct's synthesized posture marker.
//
// The posture is evaluated by the portable tier pipeline and by SecureApp.
// TierP0 is the frozen legacy pipeline and does not evaluate route-level
// postures, so an application that explicitly selects TierP0 must supply
// RequireBearerToken or own transport authentication itself.
TransportPosture TransportPosture
// RequireBearerToken validates an RFC 9728 bearer token on every MCP
// transport route. Credential-free or invalid requests are rejected with
// HTTP 401 and the RFC 9728 WWW-Authenticate challenge that points at the
// protected-resource metadata. This holds for every tier, including TierP0.
// It is valid only with TransportPostureRuntimeBearer and is rejected under
// every other posture.
RequireBearerToken *oauth.RequireBearerTokenOptions
// Provider anchors the framework-owned front channel (DESIGN §8). When it is
// set, AppTheory owns /authorize, the stage-global upstream callback, and the
// /token authorization_code grant, and the rest of the front-channel field
// set below is required. A nil Provider leaves the front channel unowned: an
// application either supplies its own AuthorizeHandler/TokenHandler pair and
// CallbackHandler, or registers no OAuth surface at all. Setting any other
// front-channel field without Provider is a registration error.
Provider oauth.Provider
// Stores supplies the five facade store SPIs the front channel persists
// through (DESIGN §7): transaction, authorization code, client,
// route-refresh broker, and rate limit. All five are required when Provider
// is set. Each store owns its own TTL (DESIGN §7 TTL column), so no TTL is
// configured here.
//
// The framework's in-memory reference stores are non-durable and do not
// encrypt: they exist for deterministic tests and local development only. A
// durable store implementation MUST encrypt the Encrypted* record fields at
// rest (for example with persistence-layer encryption), because the facade
// hands the store upstream material, including the upstream-minted access
// token, exactly as the provider returned it and relies on the store to
// protect it (DESIGN §7).
Stores oauthstore.Stores
// UpstreamClientID is the facade's own OAuth client registration at the
// upstream provider (DESIGN §8.1): the upstream authorize request carries it
// and the server-side upstream code exchange presents it. It is also the
// expected azp/client_id of an upstream access token the facade validates
// (DESIGN §5.2, §6), so a token minted for another client is refused.
UpstreamClientID string
// UpstreamIssuer is the expected upstream issuer: the RFC 9207 iss value the
// upstream callback pins before consuming the transaction or exchanging the
// code (DESIGN §8.2) and the issuer the provider's Discovery call must verify
// before any published endpoint is trusted (DESIGN §5.2, §6). It must be an
// absolute HTTPS URL with no query or fragment, normalized like IssuerURL,
// and it is distinct from the client-facing issuer.
UpstreamIssuer string
// LifecycleScopes are scope values the facade adds to every upstream
// authorize request but never requires from the MCP client, for example the
// offline_access lifecycle scope that lets the upstream issue refresh
// material (FRAMEWORK_VS_POLICY row 7). Nil adds none. Values are validated
// like Scopes: trimmed, non-empty, and well-formed Unicode.
LifecycleScopes []string
// ProviderHints supplies the opaque provider hints for one authorize request
// (DESIGN §6): it receives the request context and returns the hint map, for
// example the account_id hint. A hint carries no authority — the provider
// derives authentication account context from its own validated binding, not
// from the hint (DESIGN §6). A nil function contributes no hints; an error is
// a provider-call failure in the gateway class, so it maps to the 502
// upstream_error error page and never redirects (DESIGN §8.2).
ProviderHints func(ctx *apptheory.Context) (map[string]string, error)
}
FacadeConfig configures the complete route-algebra MCP OAuth facade.
type HandlerFactory ¶
type HandlerFactory func(mcproutes.EndpointKind) apptheory.Handler
HandlerFactory returns an application-owned handler for one endpoint kind. Factories run during facade registration, never during request handling.
type RootDiscoveryConfig ¶
type RootDiscoveryConfig struct {
IssuerURL string
AuthorizationEndpointURL string
TokenEndpointURL string
RegistrationEndpointURL string
JWKSURI string
Scopes []string
}
RootDiscoveryConfig configures the optional, unscoped authorization-server discovery document. Unlike routed discovery, every endpoint is fixed at registration time and belongs to the upstream authorization server.
type Route ¶
type Route struct {
Kind mcproutes.EndpointKind
MCPPattern string
MCPMethods []string
ProtectedResourcePattern string
DiscoveryCanonicalPattern string
DiscoverySuffixPattern string
AuthorizePattern string
TokenPattern string
RegisterPattern string
// AuthorizationRoutesAttached reports whether transport authorization is
// attached to this route family (false under the none-public posture). It
// mirrors the CDK construct's authorizationRoutesAttached inventory field,
// which is pinned by the shared facade-route-inventory fixture.
AuthorizationRoutesAttached bool
}
Route describes one endpoint-kind route family installed by the helper.
type RouteInventory ¶
type RouteInventory struct {
ContractVersion string
Routes []Route
RootAuthorizationServerPattern string
RootAuthorizationServerAttached bool
// CallbackPattern is the stage-global upstream callback path.
CallbackPattern string
// IssuerMode is the resolved per-route issuer derivation mode.
IssuerMode mcproutes.IssuerMode
}
RouteInventory is a defensive snapshot of the installed facade surface.
func RegisterMCPFacade ¶
func RegisterMCPFacade(app *apptheory.App, config FacadeConfig) (*RouteInventory, error)
RegisterMCPFacade installs the complete MCP OAuth facade described by mcproutes.ContractVersion and returns its route inventory. The helper owns composition only: authorize and token routes are absent unless the application supplies both handler factories.
The whole plan is proven installable before the app receives its first facade route, so a configuration the facade rejects, an invalid entry, or a duplicate inside the plan adds no route at all. A collision with a route the application already registered on this plain App is detected by the app's own duplicate check while the plan is mounted: the plain App surface publishes no route table, so the facade cannot pre-check that collision the way RegisterMCPFacadeSecure can on a SecureApp. See UPGRADING.md for the precise guarantee on each surface.
func RegisterMCPFacadeSecure ¶ added in v6.2.0
func RegisterMCPFacadeSecure(secure *apptheory.SecureApp, config FacadeConfig) (*RouteInventory, error)
RegisterMCPFacadeSecure installs the complete MCP OAuth facade on a closed-by-default SecureApp and returns its route inventory.
It is the SecureApp counterpart to RegisterMCPFacade and produces the same route inventory and the same request-time behavior. The difference is how each route's authority is declared: on the plain App surface the facade expresses transport security with route options, while here every route must declare an explicit auth posture, so the facade declares the posture each of its routes has always had (see the authority table in the package docs and in docs/features/mcp-facade-helper.md).
The SecureApp stays closed by default: this helper adds only the facade's own routes, each with a declared posture, and it adds none of them when the plan is rejected. The whole plan is proven installable before the app receives its first facade route — every entry, and every collision with a route the app already declares — so a returned error means zero facade routes were added. A route the application never declares remains undeliverable, and the application's own SecurePrincipalResolver, tier, and PolicyHook keep governing every non-facade route on the same app.
type TransportPosture ¶ added in v6.1.0
type TransportPosture string
TransportPosture selects how the MCP transport routes are authorized.
const ( // TransportPostureIAM keeps the authenticated transport posture (the default). TransportPostureIAM TransportPosture = "iam" // TransportPostureJWT assumes a gateway JWT authorizer already rejected the request. TransportPostureJWT TransportPosture = "jwt" // TransportPostureRuntimeBearer validates an RFC 9728 bearer token in the runtime. TransportPostureRuntimeBearer TransportPosture = "runtime-bearer" // TransportPostureNonePublic registers the transport routes without authentication. TransportPostureNonePublic TransportPosture = "none-public" )
type URLMode ¶
type URLMode string
URLMode selects the source of absolute URLs in facade metadata documents.
const ( // URLModePublicBaseURL uses FacadeConfig.PublicBaseURL fixed at registration // time. Use it when a front door or CDN owns the public origin. URLModePublicBaseURL URLMode = "public_base_url" // URLModeRequestHost derives the origin from each normalized request. Use it // for direct API Gateway custom-domain and test deployments. URLModeRequestHost URLMode = "request_host" )