Test Results
- Total
- 102
- Passed
- 102
- Failed
- 0
- Skipped
- 0
Run details
- Started
- 9/30/2026, 7:35:51 PM
- Duration
- 4.03 s
- Version
- 0.4.0
- Branch
- main
- Commit
- f94610b0
Bundling
Featuretest/bundling.test.ts
7 passed0 failed
The construct and its runtime handlers must bundle without Autotel or OpenTelemetry, so consumers who do not opt in pay nothing for tracing. Instrumentation lives in a separate entry point.
src/index.ts bundles without Autotel or OpenTelemetry
- bundling
- Giventhe entry point src/index.ts
- Whenit is bundled with esbuild
- Thenno Autotel or OpenTelemetry module is among its inputs
- Andthe output does not require autotel
src/runtime/api-key-authorizer.ts bundles without Autotel or OpenTelemetry
- bundling
src/runtime/google-authorizer.ts bundles without Autotel or OpenTelemetry
- bundling
example/mcp.ts bundles without Autotel or OpenTelemetry
- bundling
- Giventhe entry point example/mcp.ts
- Whenit is bundled with esbuild
- Thenno Autotel or OpenTelemetry module is among its inputs
- Andthe output does not require autotel
the separate Autotel entry point bundles its Lambda and MCP instrumentation
- bundling
- Giventhe example/mcp-autotel.ts entry point
- Whenit is bundled with esbuild
- Thenautotel-aws is bundled for the Lambda
- Andautotel-mcp-instrumentation is bundled for the MCP server
src/runtime/identify.ts bundles without aws-cdk-lib, so handlers can import it
- bundling
- Giventhe runtime entry point consumers import as aws-cdk-mcp/runtime
- Whenit is bundled with esbuild
- Thenno aws-cdk-lib or constructs module is among its inputs
package.json exports aws-cdk-mcp/runtime
- bundling
- Giventhe published package.json
- Then"./runtime" points at the compiled identify module
cdk-nag
Featuretest/cdk-nag.test.ts
6 passed0 failed
A consumer running AwsSolutions or Serverless checks inherits no findings from this construct, in any auth mode. What is deliberate is acknowledged inside the construct, with the reason.
auth 'iam' synthesizes clean under AwsSolutions and Serverless checks
- cdk-nag
- compliance
- Givena StatelessMcpServer with auth 'iam'
- WhenAwsSolutions and Serverless checks are added
- Thensynth raises no findings
auth 'none' synthesizes clean under AwsSolutions and Serverless checks
- cdk-nag
- compliance
- Givena StatelessMcpServer with auth 'none'
- WhenAwsSolutions and Serverless checks are added
- Thensynth raises no findings
auth 'apiKey' synthesizes clean under AwsSolutions and Serverless checks
- cdk-nag
- compliance
- Givena StatelessMcpServer with auth 'apiKey'
- WhenAwsSolutions and Serverless checks are added
- Thensynth raises no findings
auth 'jwt' synthesizes clean under AwsSolutions and Serverless checks
- cdk-nag
- compliance
- Givena StatelessMcpServer with auth 'jwt'
- WhenAwsSolutions and Serverless checks are added
- Thensynth raises no findings
auth 'lambda' synthesizes clean under AwsSolutions and Serverless checks
- cdk-nag
- compliance
- Givena StatelessMcpServer with auth 'lambda'
- WhenAwsSolutions and Serverless checks are added
- Thensynth raises no findings
auth 'googleWorkspace' synthesizes clean under AwsSolutions and Serverless checks
- cdk-nag
- compliance
- Givena StatelessMcpServer with auth 'googleWorkspace'
- WhenAwsSolutions and Serverless checks are added
- Thensynth raises no findings
Example MCP Lambda
Featuretest/example-handler.test.ts
27 passed0 failed
The example Lambda answers stateless 2026-07-28 MCP requests exactly as API Gateway delivers them. Identity comes from whichever authorizer ran (Lambda, JWT or IAM), scopes gate each tool, and anything it cannot trust is refused.
a traced request past the SDK's byte limit still gets its 413
- example
- handler
- tracing
- Givena traced tools/call of about 4.5 MB on the wire
- Whenthe traced handler receives it
- Thenit answers 413
one trace, one chain: caller → Lambda invocation → tool
- example
- handler
- tracing
- Givena caller whose request carries a W3C traceparent
- traceparent
- 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
- Whenthe traced handler runs the whoami tool
- Thenthe Lambda invocation joins the caller's trace as its child
- Andthe tool span is a child of the invocation, not a sibling
API key clients use sub as identity and receive only their granted scopes
- api-key
- example
- handler
- Givenan API key client "team-bot" granted "echo"
- Whenit calls whoami
- Thenit is identified by sub with its scopes
- Andit can call echo
- Buta "reader" client granted only "read" cannot call echo
answers a first-message tools/call with no handshake and no session
- example
- handler
- Givenbob, authorized with "read echo", and no prior initialize
- Whenthe very first message is a tools/call for whoami
- Thenit answers 200 with bob and his scopes
Lambda authorizer
- example
- handler
- scopes
JWT authorizer: a read-only token cannot call echo
- example
- handler
- scopes
no identity is refused, not treated as anonymous
- example
- handler
- Givena request with no authorizer context
- Whenit calls whoami
- Thenit is refused with 401
an identity whose token expired is refused, even if the gateway cached its approval
- example
- handler
- Givena Lambda identity and a JWT identity that both expired a second ago
- Wheneach calls whoami
- Thenboth are refused with 401
a browser Origin that is not allowlisted gets a 403
- example
- handler
- origin
- Givenno ALLOWED_ORIGINS configured
- Whena request arrives with Origin https://untrusted.example
- Thenit is refused with 403
discovery does not advertise list-changed notifications it cannot deliver
- example
- handler
- Whena client calls server/discover
- Thenit answers 200
- Buttools.listChanged is not advertised
subscriptions/listen is refused in-band, never opened as a stream
- example
- handler
- Whena client calls subscriptions/listen for toolsListChanged
- Thenno event stream is opened
- Andthe response carries a JSON-RPC error
an allowlisted browser Origin gets through
- example
- handler
- origin
- GivenALLOWED_ORIGINS is https://trusted.example
- Whena request arrives with that Origin
- Thenit answers 200
a refused identity gets a bearer challenge pointing at the metadata
- example
- handler
- Givenno RESOURCE_METADATA_URL
- Whenan unauthenticated request is refused
- AndRESOURCE_METADATA_URL is set
- Andthe same request is refused again
- Thenthe first challenge is a bare invalid_token
- Andthe second points at the protected-resource metadata
- WWW-Authenticate
- Bearer error="invalid_token", resource_metadata="https://abc.example/.well-known/oauth-protected-resource/mcp"
a trusted role acting for a listed user gets that user, and that user's scopes
- example
- handler
- iam
- GivenSlackBot is a trusted caller role, bob has "echo" and alice has "read"
- WhenSlackBot calls echo on behalf of bob, then of alice
- Thenbob gets his echo
- Butalice gets a tool error
a role not in TRUSTED_CALLER_ROLES is refused, however it got past the gateway
- example
- handler
- iam
- Givenonly SlackBot is trusted
- WhenSomeAdminRole calls on behalf of bob
- Thenit is refused with 403
- Andno OAuth bearer challenge is sent to a SigV4 caller
a delegation header outside the signature is refused: it could have been swapped
- example
- handler
- iam
- GivenSlackBot is trusted
- Whenit sends mcp-on-behalf-of without signing that header
- Thenit is refused with 403
a trusted role must say who it acts for, and for someone in USERS
- example
- handler
- iam
- GivenSlackBot is trusted and carol is not in USERS
- WhenSlackBot calls with no mcp-on-behalf-of
- Thenit is refused with 403
- AndSlackBot calls on behalf of carol
- Andit is refused with 403
a trusted role cannot act for "*", the Google everyone entry
- example
- handler
- iam
- GivenSlackBot is trusted and USERS has a "*" entry
- WhenSlackBot calls on behalf of "*"
- Thenit is refused with 403
reads SignedHeaders from the Authorization header
- example
- handler
- sigv4
reads X-Amz-SignedHeaders from a presigned URL
- example
- handler
- sigv4
- Givena presigned URL with X-Amz-SignedHeaders
- Thenthe signed header names come from the query string
no signature visible means nothing is signed
- example
- handler
- sigv4
- Givenno SigV4 signature, or a Bearer Authorization header
- Thenno headers are treated as signed
reduces a session and a pathed role to the same role ARN
- example
- handler
- iam
- Givenan assumed-role session ARN and a pathed IAM role ARN for SlackBot
- Thenboth reduce to the SlackBot role ARN
- Role ARN
- arn:aws:iam::123456789012:role/SlackBot
another account is another role, and a user is not a role
- example
- handler
- iam
- GivenSlackBot in another account
- Thenit is not the trusted SlackBot role
- Andan IAM user named SlackBot
- Andit has no role ARN
the exact origin passes, and so does no Origin at all (a server-side client)
- example
- handler
- origin
- GivenALLOWED_ORIGINS is https://trusted.example and http://localhost:5173/
- Thenexact matches and a missing Origin are allowed
Allowed Origin https://trusted.example http://localhost:5173 (none)
another scheme, port or host on the same name is refused
- example
- handler
- origin
- GivenALLOWED_ORIGINS is https://trusted.example and http://localhost:5173/
- Thennear misses are refused
Refused Origin Differs by http://trusted.example scheme https://trusted.example:8443 port https://evil.trusted.example host null opaque origin
a bare hostname in the config throws instead of admitting every scheme and port
- example
- handler
- origin
- WhenALLOWED_ORIGINS holds a bare hostname or a non-http(s) scheme
- Thenparsing throws
bearerChallenge omits resource_metadata when there is none to point at
- example
- handler
- Givenno resource metadata URL
- Thenthe challenge is a bare invalid_token
identify
Featuretest/identify.test.ts
4 passed0 failed
The handler's half of auth: turn what the gateway's authorizer attached to the request into the MCP SDK's AuthInfo, or refuse. Expiry is checked again here because API Gateway caches a Lambda authorizer's approval, which can outlive the token.
a Lambda authorizer identity becomes AuthInfo, email preferred over sub
- auth
- runtime
API Gateway passes Lambda context values as strings; exp is parsed
- auth
- runtime
- Givenexp arrives as a string
- Thenit is still honored
a JWT identity uses gateway-parsed scopes, else the scope claim
- auth
- runtime
- Givena JWT with authorizationScopes on the route
- Anda JWT without them falls back to the scope claim
refuses expired, anonymous, missing and IAM-only callers
- auth
- runtime
- Thenan identity at or past exp is refused, however recently the gateway approved it
- Andone without exp is refused
- Andone without an id is refused
- Andno authorizer at all is refused
- AndIAM callers are the handler’s own decision: refused here
Protected-resource metadata
Featuretest/protected-resource.test.ts
2 passed0 failed
OAuth clients discover the full public resource URL, including API mapping prefixes.
advertises the configured public URL even when the request uses the execute-api host
- oauth
- runtime
- Givenan endpoint mapped at /docs/mcp
- Whenthe metadata Lambda receives a request through the gateway host
- Thenclients receive the public resource and the configured authorization server
keeps request-domain discovery for deployments without publicUrl
- oauth
- runtime
- Giventhe existing configuration with no explicit public URL
- Thenthe resource still follows the request domain and MCP route
StatelessMcpServer construct
Featuretest/stateless-mcp-server.test.ts
33 passed0 failed
A stateless MCP server is one HTTP API in front of one Lambda: no sessions, no
load balancer, no cache, no table. The auth prop picks who may call /mcp:
nobody checked, API keys, a JWT issuer, IAM roles, your own authorizer, or
Google Workspace users.
has no session machinery: one API, one stage, no load balancer, no cache, no table
- cdk
- construct
- Givena server with auth "none"
- Thenthe stack has one HTTP API and no load balancer, cache or tableNo assertion
names the API after the stack, so a prefixed stack gives a prefixed API
- cdk
- construct
- Givena server in stack "Test" with id "Mcp"
- Thenthe API is named "Test-Mcp"No assertion
routes POST, GET and DELETE /mcp to the handler, throttled and access-logged
- cdk
- construct
- Givena server with auth "none"
- ThenPOST, GET and DELETE /mcp are routed without authorizationNo assertion
- Andthe default stage is throttled at 50 rps (burst 100) and access-loggedNo assertion
access logs name the caller whichever authorizer ran
- cdk
- construct
access logs can go to a log group the consumer owns, retained and longer-lived
- cdk
- construct
- Givena log group kept for a year and retained when the stack is deleted
- Whenthe server is given it as accessLogs
- Thenthe stage logs there and the construct creates no access log group of its ownNo assertion
alarms on gateway 5xx, with no action until the consumer adds one
- cdk
- construct
- Givena server with auth "none"
- Thenan alarm fires on 5 or more 5xx responses in 5 minutes
- Andit has no alarm actions
exposes the metadata URL only when it publishes metadata
- cdk
- construct
- Givena server with auth "none"
- ThenresourceMetadataUrl is undefined
- Anda server with Google Workspace auth
- AndresourceMetadataUrl points at /.well-known/oauth-protected-resource/mcp
auth 'none' publishes no protected-resource metadata
- cdk
- construct
- Givena server with auth "none"
- Thenno route serves oauth-protected-resource metadata
publishes mapped-resource discovery on the domain root API, without its default auth
- cdk
- construct
- Givenan MCP endpoint at /docs/mcp and a root API with default JWT auth
- Thenthe endpoint and metadata URLs use the full public path
- Andthe domain maps discovery at the root and the MCP stage under docs
- Andboth discovery routes are public on the root API
- AndMCP calls still use the authenticated route on the MCP API
- Andmetadata advertises the public resource instead of the gateway path
uses a root-mapped public domain without requiring a separate metadata API
- cdk
- construct
- Givena public URL whose path is the MCP route
- Thenmetadata stays on the MCP API and advertises the public domain
rejects an OAuth mapping prefix without a root metadata API
- cdk
- construct
- Givenan OAuth endpoint under /docs, with no root API for discovery
- Thenconstruction fails instead of deploying unreachable discovery
rejects public resource URLs carrying credentials, queries or fragments
- cdk
- construct
- Givena URL that cannot be an HTTPS OAuth resource identifier
- Thenconstruction refuses the URL without repeating its secrets
- Andconstruction refuses the URL without repeating its secrets
- Andconstruction refuses the URL without repeating its secrets
- Andconstruction refuses the URL without repeating its secrets
rejects a public URL without an MCP path or with a trailing slash
- cdk
- construct
- GivenpublicUrl https://docs.example.com
- Thenconstruction names the path as the problem
- AndpublicUrl https://docs.example.com/mcp/
- Andconstruction names the path as the problem
rejects metadataApi when the auth mode publishes no discovery metadata
- cdk
- construct
- GivenIAM auth with a root metadata API
- Thenconstruction fails instead of ignoring the API
apiKey protects every route using x-api-key, with no cache or OAuth metadata
- cdk
- construct
- Givena server with one API key for "team-bot"
- Thena REQUEST authorizer reads x-api-key with caching off
- Andevery /mcp route uses it
- Butno OAuth metadata is published
- Andthe authorizer receives the key hashes in API_KEYS
apiKey rejects missing or invalid hashes at synth
- cdk
- construct
- WhenapiKey auth is given no keys
- Thensynth fails
- AndapiKey auth is given a raw key instead of a sha256 hash
- Andsynth fails
jwt: API Gateway validates issuer and audience
- cdk
- construct
- Givenjwt auth with an issuer, audience "mcp" and required scope "mcp:tools"
- Thena JWT authorizer checks the issuer and audienceNo assertion
- AndPOST /mcp requires the "mcp:tools" scopeNo assertion
jwt refuses an empty requiredScopes rather than accepting ID tokens
- cdk
- construct
- Whenjwt auth is given an empty requiredScopes
- Thensynth fails naming requiredScopes
signs every /mcp route with AWS_IAM, and publishes no OAuth metadata
- cdk
- construct
- Givena server with IAM auth
- Thenevery /mcp route requires AWS_IAM
- Butno OAuth metadata is published
grantInvoke gives the role execute-api:Invoke on the /mcp routes
- cdk
- construct
- Givena server with IAM auth
- WhengrantInvoke is called with a bot role
- Thenthe role gets execute-api:Invoke on POST /mcpNo assertion
grantInvoke refuses a server whose callers are people, not roles
- cdk
- construct
- Givena server with Google Workspace auth
- WhengrantInvoke is called with a role
- Thenit throws
puts your authorizer in front of every /mcp route, with your cache TTL
- cdk
- construct
publishes protected-resource metadata only when told where clients sign in
- cdk
- construct
- Givenone server without an authorizationServer and one with it
- Thenthe first publishes no metadata
- Andthe second points clients at its authorization server
puts a cached Lambda authorizer in front of every /mcp route
- cdk
- construct
hands the authorizer the client id, domain and lower-cased users
- cdk
- construct
serves unauthenticated protected-resource metadata pointing at Google
- cdk
- construct
- Givena server with Google Workspace auth
- Thenboth well-known metadata routes are open without authorizationNo assertion
- Andthe metadata names accounts.google.com as the authorization serverNo assertion
refuses an empty clientIds, which would refuse every token
- cdk
- construct
- WhenGoogle auth is given no client ids
- Thensynth fails naming clientIds
refuses an empty hostedDomain, which would fail every cold start
- cdk
- construct
- WhenGoogle auth is given an empty hostedDomain
- Thensynth fails naming hostedDomain
refuses a users map too big for the Lambda environment
- cdk
- construct
- Given200 users, well past the 4 KB Lambda environment limit
- Thensynth fails, rather than the deploy
counts the whole authorizer environment against the 4 KB limit, not just users
- cdk
- construct
caches verdicts for cacheTtl when given, so removals bite sooner
- cdk
- construct
- GivenGoogle auth with cacheTtl of 30 seconds
- Thenthe authorizer caches for 30 secondsNo assertion
refuses an empty allowlist rather than letting the whole domain in
- cdk
- construct
- WhenGoogle auth is given no users
- Thensynth fails asking for at least one
deploys prebuilt runtime handlers, so consumers need no esbuild or Docker
- cdk
- construct
- Whena Google-auth server is synthesized
- Thenits authorizer is a plain Lambda running the bundled index.handlerNo assertion