MCP Server Facade Construct
AppTheoryMcpServer is the CDK deployment contract for the route-algebra MCP OAuth facade. Its primary mode attaches
an ordered MCP route family to a caller-owned API Gateway v2 IHttpApi; omitting api creates the same surface on a
standalone HttpApi. The construct routes traffic only. Go runtime/mcpfacade.RegisterMCPFacade owns runtime
composition, and the application owns issuer, JWKS, scopes, capabilities, and authorize/token behavior through its
explicit FacadeConfig.
This is the one path for both the four-pattern theory-mcp-server topology and a standalone specialization. It does
not accept an origin or resource URL, does not inject OAuth metadata configuration, and does not rederive OAuth routes
outside AppTheoryMcpRouteAlgebra.
Route families
A route family is an ordered patterns list. Each pattern must be a synthesis-time string whose segments are either
RFC 3986 literal path segments or complete {parameter_name} segments. Parameter names start with a letter or
underscore and continue with letters, digits, or underscores. Tokens, origins, relative paths, empty segments,
./.., mixed literal/parameter segments, greedy parameters, and duplicate patterns fail synthesis.
The default family is the contract’s ordered endpoint quartet:
const server = new AppTheoryMcpServer(this, "McpServer", {
handler,
});
| Kind | Default MCP pattern |
|---|---|
| namespace | /{client_namespace}/mcp |
| partner namespace | /{client_namespace}/partners/{partner_id}/mcp |
| agent | /{client_namespace}/agents/{agent_id}/mcp |
| partner agent | /{client_namespace}/partners/{partner_id}/agents/{agent_id}/mcp |
For every pattern, the construct calls AppTheoryMcpRouteAlgebra to derive and wire the exact runtime facade:
POST,GET, andDELETEon the MCP pattern;GETon RFC 9728 protected-resource metadata;GETon canonical and suffix-compatible RFC 8414 discovery;GETon the application-owned authorize endpoint;POSTon the application-owned token endpoint.
routeInventory exposes the contract version and the ordered derived paths. Its shape intentionally matches the
relevant fields of mcpfacade.RouteInventory. A shared fixture is asserted by both the Go helper tests and CDK synth
tests, so dropping a pattern, transport method, or facade route fails the repository gates. Set
routeFamily.rootAuthorizationServerDiscovery: true only when the runtime also supplies
FacadeConfig.RootAuthorizationServer; both sides then install the algebra-derived root discovery route.
runtime/mcpfacade.RegisterMCPFacade serves exactly this canonical four-pattern family. A noncanonical routeFamily
still uses the construct’s route algebra, but it is runtime-bring-your-own-registration: the application must register
handlers for every entry in routeInventory and must not assume RegisterMCPFacade can be configured with those
patterns. This boundary keeps the shipped helper and the deployed routes congruent instead of presenting a facade
that returns 404 for every configured path.
Stage-global upstream callback
The upstream OAuth callback is installed once per deployment rather than once per MCP kind: the callback is correlated
by the single-use upstream state the facade minted, so no per-kind callback multiplies. The construct wires one GET
route for it, exposed as routeInventory.callbackPattern.
upstreamCallbackPath overrides the path. It must be a synthesis-time literal absolute path with no query, fragment, or
dot segment, and it is normalized with the same rules as every other route path (/cb/ becomes /cb). An empty value
keeps the contract default, AppTheoryMcpRouteAlgebra.DEFAULT_CALLBACK_PATH (/_oauth/mcp/callback); a token, a
relative path, or a value that collides with another derived route fails synthesis.
new AppTheoryMcpServer(this, "McpServer", {
handler,
upstreamCallbackPath: "/_oauth/mcp/callback",
});
The construct resolves the prop through AppTheoryMcpRouteAlgebra.callbackPathForConfiguredPath, the same algebra the
runtime applies to FacadeConfig.UpstreamCallbackPath in Go, TypeScript, and Python. Supplying the same value to the
construct and to the runtime FacadeConfig keeps the CDK-mounted route and the runtime-registered route congruent
instead of letting them diverge. transportPosture: none-public attaches no OAuth surface, so it refuses
upstreamCallbackPath.
Attach mode and owned mode
Attach mode is for a shared front door. It adds HttpRoute resources to the supplied IHttpApi and creates no
AWS::ApiGatewayV2::Api:
new AppTheoryMcpServer(this, "PublicMcpFacade", {
handler: apiFunction,
api: publicHttpApi,
routeFamily: {
patterns: AppTheoryMcpRouteAlgebra.supportedEndpointTemplates().map(
(route) => route.mcpPattern,
),
rootAuthorizationServerDiscovery: true,
},
// The shared front door already owns its stage posture.
sessionState: { enabled: false },
});
The API owner owns its stage, logging, throttling, domain, CDN, and public authority. Therefore ownedApi, and the
deprecated top-level apiName, domain, and stage props, are invalid with api; synthesis names every conflicting
prop. This is the topology needed for a control plane such as theory-mcp-server, where MCP, OAuth, downloads,
webhooks, and control-plane routes share one front door.
Attach-mode endpoint and endpoints values are execute-api origin templates, not declarations of that public
authority. The construct derives their origin from api.apiId, the stack region, and the AWS URL suffix; it never
consults an apiEndpoint supplied through HttpApi.fromHttpApiAttributes. Set attachedApiStageName when the intended
stage is known so a non-$default stage such as prod appears in each template. If the stage is not supplied, the
construct cannot determine it and retains the bare execute-api origin. This accessor behavior does not change the
front door’s ownership of its public URL.
Owned mode is the standalone specialization. Omit api and optionally configure ownedApi:
new AppTheoryMcpServer(this, "StandaloneMcp", {
handler,
ownedApi: {
apiName: "cloud-keeper-mcp",
stage: { stageName: "live" },
},
});
Only owned mode may create or configure a custom domain or stage. attachedApiStageName only identifies an existing
attach-mode stage for endpoint-template derivation; it does not create, import, or mutate that stage. endpoints
returns one execute-api or owned-domain template per family member; parameterized values remain templates until
request time.
Transport posture
The full facade is wired by default, and the three MCP transport routes (POST, GET, and DELETE on every MCP
pattern) require credentials. A plain transport server defaults to an IAM authorizer; attaching facade marks the
server facade-configured and flips the default to runtime-bearer, where the gateway authorizer is NONE and the
runtime validates RFC 9728 bearer tokens. A credential-free request is then rejected by API Gateway (IAM/JWT) or by the
runtime before the application handler runs.
transportPosture selects the posture explicitly and overrides that default. The four values are iam, jwt (a static
issuer and audience through transportAuthorizer), runtime-bearer (runtime-validated RFC 9728 bearer tokens), and
none-public (deliberately public, never a default). jwt is the only posture that accepts transportAuthorizer;
supplying it with iam, runtime-bearer, or none-public fails synthesis.
runtime-bearer and none-public carry a mandatory defense-in-depth floor and fail synthesis without it:
originReachabilitymust bedisabledorlocked; a directly reachable origin is refused;runtime-bearerrequiresruntimePosture: { requireBearerToken: true }, which asserts the runtime registers the transport routes with the runtime-validated bearer posture;- in owned mode the throttling decision must be explicit (
ownedApi.stage.throttlingEnabled: true) and access logging must stay on; in attach modeowningApiDefenserecords the owning API’s access-logging and throttling decisions; runtime-beareralso requires adenialSignaldeclaration naming theapptheory.mcp.auth_deniedmetric with the boundedoutcomeandroute_familydimensions.
Discovery, protected-resource metadata, authorize, and token routes stay explicitly NONE so a supplied API’s default
authorizer cannot intercept OAuth traffic: OAuth clients must fetch those documents before they hold a token. Scope
policy and authorize/token behavior belong to the AppTheory runtime composition helper. Applications must call
RegisterMCPFacade with non-empty per-kind scopes and an application-owned authorize/token handler pair. The helper
fails closed if that pair is partial and does not install authorize/token runtime routes when both are absent.
none-public wires only the three MCP transport methods and no OAuth facade, and it cannot be combined with
transportAuthorizer, root discovery, the deprecated issuer/JWKS props, upstreamCallbackPath, or runtimePosture.
RegisterMCPFacade now registers the MCP transport routes with an authenticated posture by default and pairs its
FacadeConfig.TransportPosture with FacadeConfig.RequireBearerToken, which validates RFC 9728 bearer tokens on those
routes. Omitting runtime auth config is not an opt-out: the default CDK facade remains present and the runtime must
initialize its explicit FacadeConfig successfully.
The synthesized template records the resolved posture as CloudFormation Metadata under
apptheory:mcp-transport-posture (posture, contract_id, route_count, runtime_posture_declared) so a
conformance kit can compare a deployed posture with the runtime registration output.
The v3.1.x authorizationServerIssuer and jwksUri props are deprecated together. They remain pair-validated for
migration safety but no longer control facade wiring and no longer become environment variables. Move them into
mcpfacade.FacadeConfig.IssuerURL and .JWKSURI; scopes and capabilities move into the same app-owned struct.
Production defaults and opt-outs
Owned mode enables these defaults:
| Surface | Default | Explicit opt-out |
|---|---|---|
| DynamoDB session state | on; pay-per-request, AWS-managed encryption, PITR, TTL, retain removal policy | sessionState: { enabled: false } |
| Access logging | on; one-month retention | ownedApi.stage.accessLogging: false |
| Stage throttling | on; 100 requests/second, burst 200 | ownedApi.stage.throttlingEnabled: false |
Disabled surfaces reject their dependent options. For example, a disabled session table cannot also specify
tableName, ttlMinutes, or removalPolicy; disabled throttling cannot specify rate or burst limits. Under
runtime-bearer and none-public the access-logging and throttling opt-outs are refused outright: the owned stage must
keep access logging and an explicit throttling decision. Attach mode still creates the session table by default, but
logging and throttling belong to the supplied API’s owner. A shared front door must configure those controls on its own
stage rather than asking an attached child construct to mutate them.
Runtime helper and environment contract
The construct and runtime/mcpfacade share routes, not configuration environment variables. FacadeConfig has no env
reads. Issuer, JWKS, optional registration endpoint, URL mode, public base URL or hostname allowlist, per-kind scopes,
capabilities, MCP handler, authorize/token factories, optional root discovery, and the transport posture
(FacadeConfig.TransportPosture paired with FacadeConfig.RequireBearerToken) are app-owned values.
The construct writes only environment variables consumed by existing runtime code:
| Variable | When written | Runtime consumer |
|---|---|---|
MCP_SESSION_TABLE |
session state enabled | runtime/mcp DynamoDB session store |
MCP_SESSION_TTL_MINUTES |
session state enabled | runtime/mcp session policy |
MCP_ENDPOINT is retired in v6 in both directions: no construct writes it and no runtime reads it, so there is no shim.
The bearer challenge is app-owned configuration — a route-derived ResourceMetadataURLFunc, the per-actor
ResourceMetadataURLTemplate (TypeScript resourceMetadataURLTemplate, Python resource_metadata_url_template), or an
explicit ResourceMetadataURL (precedence: resolver > template > static) — and with none configured the middleware
advertises the bare Bearer challenge. AppTheoryRemoteMcpServer exposes the route-derived
protectedResourceMetadataUrl (the RFC 9728 metadata URL template, with {actor} preserved when actorPath is
enabled); pass it into your own app config and configure it as the template option so the middleware resolves {actor}
from the matched route. The construct emits a synth warning on the per-actor route and injects no environment variable
for this. AppTheoryMcpServer exposes its route-derived protected-resource paths through routeInventory and
protectedResourceMetadataPaths. The construct still does not accept an origin prop — adding one would reintroduce the
THE-2861 failure class.
The construct never writes the retired APPTHEORY_MCP_PATH, APPTHEORY_MCP_PROTECTED_RESOURCE_PATH,
APPTHEORY_MCP_AUTHORIZATION_SERVER_ISSUER, or APPTHEORY_MCP_JWKS_URI variables.
v3.1.x A6 migration
The A6 shape is deprecated in place for the next minor. There is no parallel legacy construct and no major-version fork.
| A6 surface | Redesign fate |
|---|---|
handler |
retained unchanged |
mcpPath prop |
deprecated; use routeFamily.patterns |
authorizationServerIssuer prop |
deprecated; move to FacadeConfig.IssuerURL |
jwksUri prop |
deprecated; move to FacadeConfig.JWKSURI |
apiName prop |
deprecated; use ownedApi.apiName |
enableSessionTable prop |
deprecated; use sessionState.enabled; default flips from off to on |
sessionTableName prop |
deprecated; use sessionState.tableName |
sessionTtlMinutes prop |
deprecated; use sessionState.ttlMinutes |
domain prop |
deprecated; use ownedApi.domain; invalid in attach mode |
stage prop |
deprecated; use ownedApi.stage; invalid in attach mode |
api accessor |
retained and generalized from HttpApi to IHttpApi |
endpoint accessor |
deprecated; use ordered endpoints |
mcpPath accessor |
deprecated; use ordered mcpPaths |
protectedResourceMetadataPath accessor |
deprecated; use protectedResourceMetadataPaths or routeInventory |
| POST-only/no-facade default | removed; full three-method OAuth facade is the default |
| session/logging/throttling off by default | removed; production defaults are on |
| issuer/JWKS env injection | removed; FacadeConfig is explicit app-owned configuration |
AppTheoryMcpProtectedResource remains only for existing REST API stacks that intentionally synthesize a static
metadata document. Its URL-valued props are deprecated and are not an alternate namespace deployment path.