diff --git a/README.md b/README.md
index c7fa36137..9e11e3046 100644
--- a/README.md
+++ b/README.md
@@ -84,6 +84,11 @@ serverless deploy
| [AWS Serverless Github Webhook Listener example in NodeJS](https://github.com/serverless/examples/tree/v4/aws-node-github-webhook-listener)
This service will listen to github webhooks fired by a given repository. | nodeJS |
| [GraphQL query endpoint in NodeJS on AWS with DynamoDB](https://github.com/serverless/examples/tree/v4/aws-node-graphql-api-with-dynamodb)
A single-module GraphQL endpoint with query and mutation functionality. | nodeJS |
| [AWS Serverless IoT Event example in NodeJS](https://github.com/serverless/examples/tree/v4/aws-node-iot-event)
This example demonstrates how to setup a AWS IoT Rule to send events to a Lambda function. | nodeJS |
+| [AWS MCP Server behind API Gateway REST (NodeJS)](https://github.com/serverless/examples/tree/v4/mcp/custom/rest-api)
Run an MCP server built with the official MCP TypeScript SDK on AWS Lambda, with streaming responses through API Gateway REST response streaming. | nodeJS |
+| [AWS MCP Server on Lambda Function URL (NodeJS)](https://github.com/serverless/examples/tree/v4/mcp/custom/function-url)
Run an MCP server built with the official MCP TypeScript SDK on a Lambda Function URL with response streaming - no API Gateway. | nodeJS |
+| [AWS MCP Server with Hono (NodeJS)](https://github.com/serverless/examples/tree/v4/mcp/custom/hono)
Run an MCP server on AWS Lambda with the official MCP Hono integration and Hono's aws-lambda adapter - no hand-written Lambda glue at all. | nodeJS |
+| [AWS MCP Server with Express and Lambda Web Adapter (NodeJS)](https://github.com/serverless/examples/tree/v4/mcp/custom/express-web-adapter)
Run an MCP server as a plain Express app on AWS Lambda with Lambda Web Adapter - zip deployment, streaming through API Gateway REST. | nodeJS |
+| [AWS MCP Server, minimal (NodeJS)](https://github.com/serverless/examples/tree/v4/mcp/minimal)
Deploy an MCP server built with the official MCP TypeScript SDK to AWS Lambda with the Serverless Framework - one module, one line of configuration. | nodeJS |
| [Node.js AWS Lambda connecting to MongoDB Atlas](https://github.com/serverless/examples/tree/v4/aws-node-mongodb-atlas)
Shows how to connect AWS Lambda to MongoDB Atlas. | nodeJS |
| [Running Puppeteer on AWS Lambda](https://github.com/serverless/examples/tree/v4/aws-node-puppeteer)
This example shows you how to run Puppeteer on AWS Lambda | nodeJS |
| [AWS Recursive Lambda function Invocation example in NodeJS](https://github.com/serverless/examples/tree/v4/aws-node-recursive-function)
This is an example of a function that will recursively call itself. | nodeJS |
@@ -167,6 +172,7 @@ serverless deploy
| [Bedrock AgentCore: LangGraph Multi-Gateway Agents (JavaScript)](https://github.com/serverless/examples/tree/v4/aws-bedrock-agentcore/javascript/langgraph-multi-gateway)
LangGraph JS agents using separate public and private AgentCore Gateways with different authorization levels. | nodeJS |
| [Bedrock AgentCore: LangGraph Agent with Token Streaming (JavaScript)](https://github.com/serverless/examples/tree/v4/aws-bedrock-agentcore/javascript/langgraph-streaming)
LangGraph JS agent streaming LLM tokens in real time over SSE via BedrockAgentCoreApp. | nodeJS |
| [Bedrock AgentCore: Standalone MCP Server (JavaScript)](https://github.com/serverless/examples/tree/v4/aws-bedrock-agentcore/javascript/mcp-server)
Standalone JavaScript MCP server deployed to AWS Bedrock AgentCore Runtime, consumable by any MCP client. | nodeJS |
+| [Bedrock AgentCore: MCP Server from Plain Lambda Functions (JavaScript)](https://github.com/serverless/examples/tree/v4/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools)
MCP server whose tools are plain Lambda functions - no MCP SDK in your code - using Bedrock AgentCore Gateway. | nodeJS |
| [Bedrock AgentCore: Strands Agent with Browser (JavaScript)](https://github.com/serverless/examples/tree/v4/aws-bedrock-agentcore/javascript/strands-browser)
Strands Agents JavaScript agent using AgentCore Browser tools for web automation. | nodeJS |
| [Bedrock AgentCore: LangGraph Basic Agent, Code Deploy (Python)](https://github.com/serverless/examples/tree/v4/aws-bedrock-agentcore/python/langgraph-basic-code)
Minimal LangGraph agent deployed to AWS Bedrock AgentCore using code (zip) deployment. | python |
| [Bedrock AgentCore: LangGraph Basic Agent, Docker Deploy (Python)](https://github.com/serverless/examples/tree/v4/aws-bedrock-agentcore/python/langgraph-basic-docker)
Minimal LangGraph agent deployed to AWS Bedrock AgentCore using Docker/container deployment. | python |
@@ -179,6 +185,9 @@ serverless deploy
| [Bedrock AgentCore: LangGraph Multi-Gateway Agents (Python)](https://github.com/serverless/examples/tree/v4/aws-bedrock-agentcore/python/langgraph-multi-gateway)
LangGraph agent using multiple AgentCore Gateways with different authorization types and tool subsets. | python |
| [Bedrock AgentCore: Strands Agent with Browser (Python)](https://github.com/serverless/examples/tree/v4/aws-bedrock-agentcore/python/strands-browser)
Strands Agents agent using AgentCore Browser for web automation and research tasks. | python |
| [Serverless Compose of Serverless, Cloudformation, and SAM](https://github.com/serverless/examples/tree/v4/compose-multiframework)
This template shows how to compose multiple services using different frameworks, in a single project | nodeJS |
+| [AWS MCP Server with Amazon Cognito OAuth (NodeJS)](https://github.com/serverless/examples/tree/v4/mcp/oauth-cognito)
Protect an MCP server with an Amazon Cognito user pool validated by API Gateway - zero token-verification code, a self-contained pool and app client, machine-to-machine tokens, with the Serverless Framework. | nodeJS |
+| [AWS MCP Server with a Custom Lambda Authorizer and Auth0 (NodeJS)](https://github.com/serverless/examples/tree/v4/mcp/oauth-authorizer)
Reject unauthorized MCP traffic at API Gateway before the Lambda runs, using your own Lambda authorizer with Auth0 as the identity provider, with the Serverless Framework. | nodeJS |
+| [AWS MCP Server Verifying OAuth In-Module (NodeJS)](https://github.com/serverless/examples/tree/v4/mcp/oauth-in-module)
Verify OAuth 2.1 bearer tokens inside your MCP server module with the MCP SDK's requireBearerAuth gate - spec-shaped 401 challenges, scope-aware 403s, and the caller's identity in your tools, with the Serverless Framework. | nodeJS |
## Community Examples
diff --git a/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/README.md b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/README.md
new file mode 100644
index 000000000..75b51ae2f
--- /dev/null
+++ b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/README.md
@@ -0,0 +1,72 @@
+
+
+# MCP Server from Plain Lambda Functions
+
+Deploy a fully managed [Model Context Protocol](https://modelcontextprotocol.io) server whose tools are **plain Lambda functions** — no MCP SDK, no HTTP server, and no protocol code anywhere in your project.
+
+The Serverless Framework provisions a [Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html), which *is* the MCP server: an AWS-managed `/mcp` endpoint that answers `tools/list` and `server/discover` itself and performs one Lambda invocation per tool call. Your handlers receive the tool arguments as the event and return the bare result:
+
+```js
+export const handler = async (event) => event.a + event.b
+```
+
+With `supportedVersions: ['2026-07-28', ...]`, the endpoint serves the stateless MCP protocol revision — self-contained requests with no handshake and no sessions — alongside earlier revisions on the same URL.
+
+**How this differs from the [`mcp-server`](../mcp-server) example:** that one deploys *your own* MCP server (built with the MCP SDK, running as a container on AgentCore Runtime) — full control over the protocol, streaming, and sessions, in exchange for owning the server code. This example inverts the trade: AWS runs the MCP server for you and your code shrinks to plain functions, with the tool schema declared in `serverless.yml`. Pick `mcp-server` when you need SDK-level capabilities; pick this one when your tools are simple request/response functions.
+
+## Usage
+
+### Deploy
+
+```bash
+serverless deploy
+```
+
+The deploy output prints the gateway's MCP endpoint:
+
+```
+agents:
+ mcpServer: https://mcp-server-lambda-tools-mcpserver-....gateway.bedrock-agentcore.us-east-1.amazonaws.com/mcp
+```
+
+### Test
+
+The gateway uses AWS_IAM (SigV4) authorization by default. The included test client signs requests with your local AWS credentials:
+
+```bash
+npm install
+GATEWAY_URL= node test-client.mjs
+```
+
+It calls `server/discover`, `tools/list`, and `tools/call`:
+
+```
+=== tools/call (calculator___add) -> HTTP 200 ===
+{"result":{"isError":false,"content":[{"type":"text","text":"42"}],"resultType":"complete"},...}
+```
+
+Note that tool names are namespaced by their gateway target: the `add` tool of the `calculator` target is invoked as `calculator___add`.
+
+### Connect MCP clients
+
+MCP clients such as Claude or AI IDEs authenticate with OAuth rather than SigV4. To support them, configure a JWT authorizer on the gateway (see the commented `authorizer` block in `serverless.yml`) with your OpenID Connect provider's discovery URL — for example an Amazon Cognito user pool.
+
+### Add tools
+
+Each entry in `ai.tools` maps a schema to a function. A single `toolSchema` array can declare multiple tools backed by one function — the invoked tool name is available in `context.clientContext.custom['bedrockAgentCoreToolName']`. Beyond Lambda, gateway tools can also wrap OpenAPI definitions (`openapi: ./spec.yml`) or proxy external MCP servers (`mcp: https://example.com/mcp`).
+
+### Cleanup
+
+```bash
+serverless remove
+```
diff --git a/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/handlers/calculator.mjs b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/handlers/calculator.mjs
new file mode 100644
index 000000000..514a3d37b
--- /dev/null
+++ b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/handlers/calculator.mjs
@@ -0,0 +1,21 @@
+/**
+ * A plain Lambda handler exposed as an MCP tool through AgentCore Gateway.
+ *
+ * The Gateway passes the tool's input arguments as the event and embeds the
+ * return value as the MCP tool result - return the bare result directly.
+ *
+ * When one function backs multiple tools, the invoked tool name arrives in
+ * context.clientContext.custom['bedrockAgentCoreToolName'] in the form
+ * `___` (for example `calculator___add`).
+ */
+export const handler = async (event, context) => {
+ const { a, b } = event
+ if (typeof a !== 'number' || typeof b !== 'number') {
+ throw new Error('a and b must be numbers')
+ }
+ // `___` - branch on the tool name when one function backs
+ // several tools:
+ const tool = context.clientContext?.custom?.bedrockAgentCoreToolName ?? ''
+ if (tool.endsWith('___multiply')) return a * b
+ return a + b
+}
diff --git a/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/package-lock.json b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/package-lock.json
new file mode 100644
index 000000000..9175d62af
--- /dev/null
+++ b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/package-lock.json
@@ -0,0 +1,469 @@
+{
+ "name": "mcp-server-lambda-tools",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mcp-server-lambda-tools",
+ "version": "1.0.0",
+ "devDependencies": {
+ "@aws-crypto/sha256-js": "^5.2.0",
+ "@aws-sdk/credential-provider-node": "^3.750.0",
+ "@smithy/protocol-http": "^5.0.0",
+ "@smithy/signature-v4": "^5.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-js": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
+ "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/util": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
+ "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.222.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-sdk/core": {
+ "version": "3.977.1",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.1.tgz",
+ "integrity": "sha512-KVtQRtc00ES/y+Sc3vYXeP6pCIcNlBJCZOwvqSy8ZpVGmbM5+IG+AfhuTKQ2oXmIVqZJewaGMMpzPkywC6xg0w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.974.2",
+ "@aws-sdk/xml-builder": "^3.972.37",
+ "@aws/lambda-invoke-store": "^0.3.0",
+ "@smithy/core": "^3.29.8",
+ "@smithy/signature-v4": "^5.6.9",
+ "@smithy/types": "^4.16.1",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.972.61",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.61.tgz",
+ "integrity": "sha512-qihs2ekMb89Nxd2JenCgVFhjbkb3EIo7HEBCBzyZACKVJdrLUZBLOmAE3xr0Sayml8n/jZSzwO/IufIiIzO7PQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.977.0",
+ "@aws-sdk/types": "^3.974.2",
+ "@smithy/core": "^3.29.8",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.972.63",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.63.tgz",
+ "integrity": "sha512-yfozsS8wkWZEi/n6IsrodcFKBWZ0iNAezhJbTReMNc0z1Px17qdeAeuL1/wziCAmCZyXiW7QzP75ggJkBQv8jQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.977.0",
+ "@aws-sdk/types": "^3.974.2",
+ "@smithy/core": "^3.29.8",
+ "@smithy/fetch-http-handler": "^5.6.10",
+ "@smithy/node-http-handler": "^4.9.10",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.973.6",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.6.tgz",
+ "integrity": "sha512-jGLTW1bj148GL/6/IMlfY2fMYS9FtHOG+NahkFD4y0qkzYudNUahelxryY68/HGMslYuHClk1XaS/3b3eJzEkg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.977.0",
+ "@aws-sdk/credential-provider-env": "^3.972.61",
+ "@aws-sdk/credential-provider-http": "^3.972.63",
+ "@aws-sdk/credential-provider-login": "^3.972.68",
+ "@aws-sdk/credential-provider-process": "^3.972.61",
+ "@aws-sdk/credential-provider-sso": "^3.973.5",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.67",
+ "@aws-sdk/nested-clients": "^3.997.35",
+ "@aws-sdk/types": "^3.974.2",
+ "@smithy/core": "^3.29.8",
+ "@smithy/credential-provider-imds": "^4.4.13",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-login": {
+ "version": "3.972.68",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.68.tgz",
+ "integrity": "sha512-w6tNci6g7RqFpLhj1f5xseBvaNojb4Pkgp5Jp5apl9hrJtaf2AA+rX9+qlhlWUK6kcyAFYPA7emO+55zj+S98Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.977.0",
+ "@aws-sdk/nested-clients": "^3.997.35",
+ "@aws-sdk/types": "^3.974.2",
+ "@smithy/core": "^3.29.8",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.972.72",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.72.tgz",
+ "integrity": "sha512-blQ7F5QGzylnzeh5549zQLoCAiMHkXFLjFovEMaVy4b2X8JhUu+u9NXro1hyK95YHdVFNmBHKs2hIHtZchxKlQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "^3.972.61",
+ "@aws-sdk/credential-provider-http": "^3.972.63",
+ "@aws-sdk/credential-provider-ini": "^3.973.6",
+ "@aws-sdk/credential-provider-process": "^3.972.61",
+ "@aws-sdk/credential-provider-sso": "^3.973.5",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.67",
+ "@aws-sdk/types": "^3.974.2",
+ "@smithy/core": "^3.29.8",
+ "@smithy/credential-provider-imds": "^4.4.13",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.972.61",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.61.tgz",
+ "integrity": "sha512-xzRuj+fUVO4nkafKQJVKAF97kGpeQbfjuwmRrtGZNf42/1dkmcz6o7dswBy7alY0htQn5sCL1GWQYEykviWZkA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.977.0",
+ "@aws-sdk/types": "^3.974.2",
+ "@smithy/core": "^3.29.8",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.973.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.5.tgz",
+ "integrity": "sha512-fZRjjWhLFelsDoOYjqShQTrIGYC3Pf9Mx9Czf+1ikfQDgktxjze33dVo1q1/ZQ+T0qbtejVoHNHrfD5aJVpv/w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.977.0",
+ "@aws-sdk/nested-clients": "^3.997.35",
+ "@aws-sdk/token-providers": "3.1095.0",
+ "@aws-sdk/types": "^3.974.2",
+ "@smithy/core": "^3.29.8",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.972.67",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.67.tgz",
+ "integrity": "sha512-FTNZ05gkPBA6CKbU3N4zPgybV+stdazwMOya75CmGdcJL7p8Fw/BdHP8WVxJd0mvzyPK2cg/C3gli58Ir4HgCw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.977.0",
+ "@aws-sdk/nested-clients": "^3.997.35",
+ "@aws-sdk/types": "^3.974.2",
+ "@smithy/core": "^3.29.8",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients": {
+ "version": "3.997.35",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.35.tgz",
+ "integrity": "sha512-2MJfseVG/aXvIyOIBlYA/Oaf6qFDdsu4D8RKsEUdOQpVuLaor0BdxIBBtJLBNQQEe6Ku3YMvLljwb1MwVUpzRw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.977.0",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.42",
+ "@aws-sdk/types": "^3.974.2",
+ "@smithy/core": "^3.29.8",
+ "@smithy/fetch-http-handler": "^5.6.10",
+ "@smithy/node-http-handler": "^4.9.10",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.996.42",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.42.tgz",
+ "integrity": "sha512-DBV4naZP6HYBlAvPpoQzOP12Wvfou/5rN8yJPXjBTBylU5qwCbh/tXr2MddHoIjgoRkEl/eS+IljiUqvmwey1Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.974.2",
+ "@smithy/signature-v4": "^5.6.9",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/token-providers": {
+ "version": "3.1095.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1095.0.tgz",
+ "integrity": "sha512-65SudS6y4nzaYHybtqcpm3sHe5jLhdMn68HRKS1nUx690BtQeaAQOoujQ+dpOjBATIVGVgKKjEP8tR+U06QJQA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.977.0",
+ "@aws-sdk/nested-clients": "^3.997.35",
+ "@aws-sdk/types": "^3.974.2",
+ "@smithy/core": "^3.29.8",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/types": {
+ "version": "3.974.2",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.2.tgz",
+ "integrity": "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/xml-builder": {
+ "version": "3.972.37",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.37.tgz",
+ "integrity": "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws/lambda-invoke-store": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
+ "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/core": {
+ "version": "3.30.0",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.30.0.tgz",
+ "integrity": "sha512-dl2yRglDxfzH9uJ4fSo4zTaAHa0zH7+V7BZMRWy8hEYIKT1BiqMUK/CN6T3ADQ3kbA5N1tmUulroJ2UtONS7Kw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/credential-provider-imds": {
+ "version": "4.4.14",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.14.tgz",
+ "integrity": "sha512-QgbuahIb2qxQeZQvNK0sw3aF3JH5zwH8j2lLp5DUasVXexGGMWULAR+7z0omPXFolCP/m5wN9M5lm9EGdSviTQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.30.0",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/fetch-http-handler": {
+ "version": "5.6.11",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.11.tgz",
+ "integrity": "sha512-o0Zkj1nKqJAoq+a+BrkhU39tRftMNjLwpc/z06Frfl43wpbHrJMaSAVZE4vTqlxtVkNaGaT0bIDxOp7tkFTuQQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.30.0",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/is-array-buffer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
+ "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/node-http-handler": {
+ "version": "4.9.11",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.11.tgz",
+ "integrity": "sha512-slbzbz8taEOzoXv/9y34YNBoE+ZHmddLykCgjDAjvMAsu2nM5s2Gzwa5OGF721tD8s+CKeFUBds5lSj9lcbuDg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.30.0",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/protocol-http": {
+ "version": "5.5.14",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.5.14.tgz",
+ "integrity": "sha512-R+aYfS8kDXMwO+LfpiWTJjjUsR+se4cd2ZBjMU3fqJmoSZ1jenTpgb4/k1SvvXcM9f0232KvufZoO5XRWoiPBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.30.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/signature-v4": {
+ "version": "5.6.10",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.10.tgz",
+ "integrity": "sha512-EXhWePm3SXJAX38npIy4TXL2Aex/OVgCClTjelN2QHw/U+8CQUH8C7AaxsVzQcYieYPseywn48s89DmvuGjiAg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.30.0",
+ "@smithy/types": "^4.16.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/types": {
+ "version": "4.16.1",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz",
+ "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-buffer-from": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
+ "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-utf8": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
+ "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/bowser": {
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
+ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ }
+ }
+}
diff --git a/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/package.json b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/package.json
new file mode 100644
index 000000000..44c6b41b4
--- /dev/null
+++ b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "mcp-server-lambda-tools",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "MCP server from plain Lambda functions via Bedrock AgentCore Gateway",
+ "devDependencies": {
+ "@aws-crypto/sha256-js": "^5.2.0",
+ "@aws-sdk/credential-provider-node": "^3.750.0",
+ "@smithy/protocol-http": "^5.0.0",
+ "@smithy/signature-v4": "^5.0.0"
+ }
+}
diff --git a/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/serverless.yml b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/serverless.yml
new file mode 100644
index 000000000..62d556623
--- /dev/null
+++ b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/serverless.yml
@@ -0,0 +1,67 @@
+# MCP server from plain Lambda functions via Bedrock AgentCore Gateway.
+#
+# The Gateway is the MCP server: an AWS-managed /mcp endpoint that serves
+# tools/list and server/discover itself and invokes a Lambda function per
+# tool call. Tool handlers are plain functions - no MCP SDK, no HTTP server.
+service: mcp-server-lambda-tools
+
+provider:
+ name: aws
+
+functions:
+ calculator:
+ handler: handlers/calculator.handler
+ runtime: nodejs24.x
+
+ai:
+ tools:
+ calculator:
+ function: calculator
+ toolSchema: # the Gateway serves tools/list from this schema
+ - name: add
+ description: Add two numbers together
+ inputSchema:
+ type: object
+ properties:
+ a: { type: number, description: First number }
+ b: { type: number, description: Second number }
+ required: [a, b]
+ # One function can back multiple tools - the invoked tool name arrives
+ # in context.clientContext.custom['bedrockAgentCoreToolName'] and the
+ # handler branches on it (see handlers/calculator.mjs):
+ # - name: multiply
+ # description: Multiply two numbers
+ # inputSchema:
+ # type: object
+ # properties:
+ # a: { type: number }
+ # b: { type: number }
+ # required: [a, b]
+
+ # Tools don't have to be Lambda functions. The same gateway can expose
+ # an HTTP API described by an OpenAPI document:
+ # weather:
+ # openapi: ./weather-openapi.yml
+ # ...or proxy an external MCP server (streaming and elicitation pass
+ # through for this target type):
+ # external:
+ # mcp: https://mcp.example.com/mcp
+
+ gateways:
+ mcpServer:
+ protocol:
+ # Advertise the MCP protocol revisions this endpoint serves.
+ supportedVersions: ['2026-07-28', '2025-03-26']
+ # With many tools, semantic search helps agents find the right one
+ # (adds a built-in x_amz_bedrock_agentcore_search tool):
+ # searchType: SEMANTIC
+ # instructions: 'Use the calculator tools for any arithmetic'
+ tools: [calculator]
+ # Default authorization is AWS_IAM (SigV4) - see test-client.mjs.
+ # For OAuth clients (Claude, AI IDEs), configure a JWT authorizer with
+ # your OpenID Connect provider instead:
+ # authorizer:
+ # type: CUSTOM_JWT
+ # jwt:
+ # discoveryUrl: https://cognito-idp.us-east-1.amazonaws.com//.well-known/openid-configuration
+ # allowedAudience: []
diff --git a/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/test-client.mjs b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/test-client.mjs
new file mode 100644
index 000000000..c128aa10a
--- /dev/null
+++ b/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools/test-client.mjs
@@ -0,0 +1,62 @@
+#!/usr/bin/env node
+/**
+ * Test client for the deployed MCP gateway (AWS_IAM authorization).
+ * Signs requests with SigV4 using your local AWS credentials.
+ *
+ * Usage:
+ * GATEWAY_URL= node test-client.mjs
+ */
+import { SignatureV4 } from '@smithy/signature-v4'
+import { HttpRequest } from '@smithy/protocol-http'
+import { Sha256 } from '@aws-crypto/sha256-js'
+import { defaultProvider } from '@aws-sdk/credential-provider-node'
+
+const GATEWAY_URL = process.env.GATEWAY_URL
+if (!GATEWAY_URL) {
+ console.error('Set GATEWAY_URL to the URL printed by `serverless deploy`')
+ process.exit(1)
+}
+const url = new URL(GATEWAY_URL)
+const REGION = process.env.AWS_REGION || 'us-east-1'
+
+const signer = new SignatureV4({
+ service: 'bedrock-agentcore',
+ region: REGION,
+ credentials: defaultProvider(),
+ sha256: Sha256,
+})
+
+const META = {
+ 'io.modelcontextprotocol/protocolVersion': '2026-07-28',
+ 'io.modelcontextprotocol/clientInfo': { name: 'test-client', version: '1.0.0' },
+ 'io.modelcontextprotocol/clientCapabilities': {},
+}
+
+let id = 0
+async function rpc(method, params, name) {
+ const body = JSON.stringify({ jsonrpc: '2.0', id: ++id, method, params })
+ const request = new HttpRequest({
+ method: 'POST',
+ protocol: url.protocol,
+ hostname: url.hostname,
+ path: url.pathname,
+ headers: {
+ host: url.hostname,
+ 'content-type': 'application/json',
+ accept: 'application/json, text/event-stream',
+ 'mcp-protocol-version': '2026-07-28',
+ 'mcp-method': method,
+ ...(name && { 'mcp-name': name }),
+ },
+ body,
+ })
+ const signed = await signer.sign(request)
+ const res = await fetch(GATEWAY_URL, { method: 'POST', headers: signed.headers, body })
+ console.log(`\n=== ${method}${name ? ` (${name})` : ''} -> HTTP ${res.status} ===`)
+ console.log((await res.text()).slice(0, 1200))
+}
+
+await rpc('server/discover', { _meta: META })
+await rpc('tools/list', { _meta: META })
+// Tool names are namespaced by their gateway target: ___
+await rpc('tools/call', { name: 'calculator___add', arguments: { a: 2, b: 40 }, _meta: META }, 'calculator___add')
diff --git a/aws-bedrock-agentcore/javascript/mcp-server/README.md b/aws-bedrock-agentcore/javascript/mcp-server/README.md
index d6834bae8..a87357a9f 100644
--- a/aws-bedrock-agentcore/javascript/mcp-server/README.md
+++ b/aws-bedrock-agentcore/javascript/mcp-server/README.md
@@ -12,27 +12,24 @@ authorAvatar: 'https://avatars1.githubusercontent.com/u/13742415?s=200&v=4'
# MCP Server Example
-A JavaScript MCP server deployed to AWS Bedrock AgentCore Runtime. Exposes simple tools via the [Model Context Protocol](https://modelcontextprotocol.io/) that can be consumed by any MCP client (Cursor, Claude Desktop, Amazon Q CLI, etc.).
+A JavaScript MCP server deployed to AWS Bedrock AgentCore Runtime. Exposes tools via the [Model Context Protocol](https://modelcontextprotocol.io/) that can be consumed by any MCP client (Cursor, Claude Desktop, Amazon Q CLI, etc.).
-## Tools
-
-| Tool | Description |
-| ------------------ | ------------------------------------------------------ |
-| `add` | Add two numbers together |
-| `multiply` | Multiply two numbers together |
-| `get_current_time` | Get the current date and time (with optional timezone) |
+It serves the same canonical MCP server as the [aws-mcp-servers](../../../aws-mcp-servers/README.md) example family - tools (`add`, `slow_report` with streamed progress, `approve_refund` with an elicitation round-trip), resources (`guide://usage` plus an `orders://{orderId}` template), a prompt, `instructions` served via `server/discover`, and cache hints - so the family's shared test client works here too, and you can compare hosting on AgentCore Runtime against the Lambda-based options side by side.
## Prerequisites
- Node.js 24.x
- AWS account with credentials configured
- Serverless Framework installed (`npm i -g serverless`)
+- Docker running (the runtime is deployed as a container)
## Project structure
```text
mcp-server/
-├── index.js # MCP server (Express + @modelcontextprotocol/sdk)
+├── index.js # HTTP layer: node:http + toNodeHandler on port 8000
+├── src/server.mjs # the canonical MCP server (official MCP SDK v2)
+├── client.mjs # the family's shared test client
├── package.json # Dependencies and start script
├── serverless.yml # Serverless Framework configuration
└── README.md
@@ -45,6 +42,24 @@ npm install
sls deploy
```
+## Test
+
+The deploy output prints the runtime's MCP endpoint URL. The shared client calls it directly - the same way real MCP clients connect - signing requests with your local AWS credentials (SigV4, the runtime's default inbound auth):
+
+```bash
+AUTH=sigv4 SERVICE=bedrock-agentcore ENDPOINT= node client.mjs
+```
+
+```
+PASS 1. tools/list returns the canonical tools with cache hints — ttlMs=300000 cacheScope=public
+PASS 2. add returns plain JSON with structured content — sum=42
+PASS 3. slow_report streams incremental progress over SSE — 3 progress events, first at +810ms
+PASS 4. approve_refund elicitation round-trip (accept and cancel) — input_required -> refunded o-1 / refund cancelled
+...
+```
+
+Note the `requestHeaders.allowlist` block in `serverless.yml`: protocol revision `2026-07-28` requires the `Mcp-Method` and `Mcp-Name` headers on every request, and the allowlist forwards them to the container.
+
## Local development
```bash
@@ -54,9 +69,11 @@ sls dev
## How it works
-The server uses Express to expose a stateless Streamable HTTP endpoint at `POST /mcp` on port 8000, which is the standard expected by AgentCore Runtime for MCP-protocol runtimes. Each request creates a fresh MCP server instance, processes the JSON-RPC message, and cleans up on close.
+The server is built with the official [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) v2: `createMcpHandler` produces a web-standard handler serving the stateless MCP `2026-07-28` protocol revision (self-contained requests, `server/discover`, structured results) while answering older MCP clients through the SDK's built-in fallback on the same endpoint. `toNodeHandler` mounts it on a plain `node:http` server at `/mcp` on port 8000, the standard expected by AgentCore Runtime for MCP-protocol runtimes. The Runtime adds an `Mcp-Session-Id` header for its own session isolation; the stateless server accepts and ignores it.
+
+The `serverless.yml` sets `protocol: MCP` on the agent, which tells AgentCore to route MCP traffic to the runtime. A commented `authorizer.jwt` block shows platform OAuth: the runtime validates Bearer JWTs from any OpenID Connect provider before requests reach the container - all configuration, no code.
-The `serverless.yml` sets `protocol: MCP` on the agent, which tells AgentCore to route MCP traffic to the runtime.
+**Related example:** [`mcp-server-lambda-tools`](../mcp-server-lambda-tools) inverts this trade — AWS runs the MCP server (AgentCore Gateway) and your code shrinks to plain Lambda functions with the tool schema declared in `serverless.yml`. Pick this example when you need full SDK-level control; pick that one for simple request/response tools with no MCP code at all.
## Connecting MCP clients
diff --git a/aws-bedrock-agentcore/javascript/mcp-server/client.mjs b/aws-bedrock-agentcore/javascript/mcp-server/client.mjs
new file mode 100644
index 000000000..7ea0a3700
--- /dev/null
+++ b/aws-bedrock-agentcore/javascript/mcp-server/client.mjs
@@ -0,0 +1,348 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the mcp/custom examples. Every example serves
+ * the same canonical MCP server, so this same script tests all of them.
+ *
+ * Usage:
+ * ENDPOINT= node client.mjs
+ * LONG=1 ENDPOINT=... node client.mjs # adds a ~36s streaming case
+ * AUTH=sigv4 SERVICE=bedrock-agentcore ENDPOINT= node client.mjs
+ * AUTH=sigv4 SERVICE=lambda ENDPOINT=... node client.mjs # IAM-auth Function URL
+ *
+ * Exercises the full 2026-07-28 surface: plain JSON, SSE progress streaming,
+ * an elicitation round-trip, resources (static + template), prompts,
+ * server/discover, cache hints, the legacy fallback, and two negative cases
+ * (header mismatch -32020, missing capability -32021).
+ */
+
+const ENDPOINT = process.env.ENDPOINT
+if (!ENDPOINT) {
+ console.error('Set ENDPOINT to the MCP endpoint URL')
+ process.exit(1)
+}
+
+// --- endpoint + optional SigV4 signing --------------------------------------
+
+let url = new URL(ENDPOINT)
+let signer
+if (process.env.AUTH === 'sigv4') {
+ const service = process.env.SERVICE || 'lambda'
+ if (service === 'bedrock-agentcore' && url.pathname.includes('/runtimes/')) {
+ // AgentCore runtime URLs embed the agent ARN - the WHOLE ARN must be
+ // URL-encoded in the path (including its inner "/").
+ const arn = decodeURIComponent(url.pathname.match(/\/runtimes\/(.+?)\/invocations/)[1])
+ url = new URL(`${url.origin}/runtimes/${encodeURIComponent(arn)}/invocations`)
+ url.searchParams.set('qualifier', 'DEFAULT')
+ }
+ const [{ SignatureV4 }, { HttpRequest }, { Sha256 }, { defaultProvider }] = await Promise.all([
+ import('@smithy/signature-v4'),
+ import('@smithy/protocol-http'),
+ import('@aws-crypto/sha256-js'),
+ import('@aws-sdk/credential-provider-node'),
+ ])
+ const sigv4 = new SignatureV4({
+ service,
+ region: process.env.AWS_REGION || 'us-east-1',
+ credentials: defaultProvider(),
+ sha256: Sha256,
+ })
+ signer = async (method, headers, body) => {
+ const request = new HttpRequest({
+ method,
+ protocol: url.protocol,
+ hostname: url.hostname,
+ path: url.pathname,
+ query: Object.fromEntries(url.searchParams),
+ headers: { host: url.hostname, ...headers },
+ ...(body != null && { body }),
+ })
+ return (await sigv4.sign(request)).headers
+ }
+}
+
+// --- request helpers ---------------------------------------------------------
+
+const CAPS_ELICIT = { elicitation: { form: {} } }
+const meta = (capabilities = {}) => ({
+ 'io.modelcontextprotocol/protocolVersion': '2026-07-28',
+ 'io.modelcontextprotocol/clientInfo': { name: 'aws-mcp-servers-client', version: '1.0.0' },
+ 'io.modelcontextprotocol/clientCapabilities': capabilities,
+})
+
+let id = 0
+async function request({ method, params = {}, name, capabilities, headerMethod, envelope = true }) {
+ const body = JSON.stringify({
+ jsonrpc: '2.0',
+ id: ++id,
+ method,
+ params: envelope ? { ...params, _meta: { ...meta(capabilities), ...(params._meta ?? {}) } } : params,
+ })
+ let headers = {
+ 'content-type': 'application/json',
+ accept: 'application/json, text/event-stream',
+ ...(envelope && {
+ 'mcp-protocol-version': '2026-07-28',
+ 'mcp-method': headerMethod ?? method,
+ ...(name && { 'mcp-name': name }),
+ }),
+ }
+ if (signer) headers = await signer('POST', headers, body)
+ const t0 = Date.now()
+ const res = await fetch(url, { method: 'POST', headers, body })
+ const contentType = res.headers.get('content-type') ?? ''
+ if (contentType.includes('text/event-stream')) {
+ const events = []
+ const decoder = new TextDecoder()
+ let buffer = ''
+ for await (const chunk of res.body) {
+ buffer += decoder.decode(chunk, { stream: true })
+ let sep
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
+ const frame = buffer.slice(0, sep)
+ buffer = buffer.slice(sep + 2)
+ const data = frame.split('\n').find((l) => l.startsWith('data:'))?.slice(5).trim()
+ if (data) events.push({ t: Date.now() - t0, data: JSON.parse(data) })
+ }
+ }
+ const final = events.at(-1)?.data
+ return { status: res.status, contentType, events, json: final, ms: Date.now() - t0 }
+ }
+ const text = await res.text()
+ let json
+ try {
+ json = JSON.parse(text)
+ } catch {
+ json = { raw: text }
+ }
+ return { status: res.status, contentType, events: [], json, ms: Date.now() - t0 }
+}
+
+// --- tiny harness ------------------------------------------------------------
+
+const results = []
+async function check(label, fn) {
+ try {
+ const detail = await fn()
+ results.push({ label, ok: true })
+ console.log(`PASS ${label}${detail ? ` — ${detail}` : ''}`)
+ } catch (err) {
+ results.push({ label, ok: false })
+ console.log(`FAIL ${label} — ${err.message}`)
+ }
+}
+const assert = (cond, msg) => {
+ if (!cond) throw new Error(msg)
+}
+// AgentCore Runtime replaces 4xx response bodies with its own error envelope
+// (-32010, "Received error (400) ..."); the 400 status still proves the SDK
+// rejected the request, so the negative cases accept that form there.
+const rejectedWith = (json, code) =>
+ json.error?.code === code ||
+ (process.env.SERVICE === 'bedrock-agentcore' &&
+ json.error?.code === -32010 &&
+ /\(4\d\d\)/.test(json.error?.message ?? ''))
+
+// --- test cases ----------------------------------------------------------------
+
+await check('1. tools/list returns the canonical tools with cache hints', async () => {
+ const r = await request({ method: 'tools/list' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ const names = (r.json.result?.tools ?? []).map((t) => t.name).sort()
+ assert(JSON.stringify(names) === JSON.stringify(['add', 'approve_refund', 'slow_report']), `tools: ${names}`)
+ assert(r.json.result.ttlMs === 300000, `ttlMs ${r.json.result.ttlMs}`)
+ assert(r.json.result.cacheScope === 'public', `cacheScope ${r.json.result.cacheScope}`)
+ return `ttlMs=${r.json.result.ttlMs} cacheScope=${r.json.result.cacheScope}`
+})
+
+await check('2. add returns plain JSON with structured content', async () => {
+ const r = await request({ method: 'tools/call', params: { name: 'add', arguments: { a: 2, b: 40 } }, name: 'add' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ assert(r.json.result?.structuredContent?.sum === 42, JSON.stringify(r.json))
+ return 'sum=42'
+})
+
+await check('3. slow_report streams incremental progress over SSE', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 3 }, _meta: { progressToken: 'pt' } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('text/event-stream'), `content-type ${r.contentType}`)
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 3, `${progress.length} progress events`)
+ assert(progress[0].t < r.ms - 500, 'first progress event did not arrive before the final result')
+ assert(r.json.result?.content?.[0]?.text === 'completed 3 steps', JSON.stringify(r.json))
+ return `3 progress events, first at +${progress[0].t}ms, done in ${r.ms}ms`
+})
+
+await check('4. approve_refund elicitation round-trip (accept and cancel)', async () => {
+ const ask = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-1' } },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(ask.json.result?.resultType === 'input_required', JSON.stringify(ask.json))
+ assert(ask.json.result?.inputRequests?.confirm, 'missing inputRequests.confirm')
+ const accepted = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: true } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(accepted.json.result?.content?.[0]?.text === 'refunded o-1', JSON.stringify(accepted.json))
+ const cancelled = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: false } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(cancelled.json.result?.content?.[0]?.text === 'refund cancelled', JSON.stringify(cancelled.json))
+ // A real decline ACTION, not accept-with-false: a server missing the
+ // inputResponse action check re-asks on decline, and a real client loops.
+ const declined = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'decline' } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(declined.json.result?.content?.[0]?.text === 'refund cancelled', `decline must terminate, not re-ask: ${JSON.stringify(declined.json)}`)
+ return 'input_required -> refunded o-1 / refund cancelled / decline terminates'
+})
+
+await check('5. resources list + read (Mcp-Name carries the uri)', async () => {
+ const list = await request({ method: 'resources/list' })
+ const uris = (list.json.result?.resources ?? []).map((r) => r.uri)
+ assert(uris.includes('guide://usage'), `resources: ${uris}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'guide://usage' }, name: 'guide://usage' })
+ assert(read.json.result?.contents?.[0]?.text?.startsWith('# Usage'), JSON.stringify(read.json))
+ return 'guide://usage readable'
+})
+
+await check('6. resource template read with per-resource cache hint', async () => {
+ const templates = await request({ method: 'resources/templates/list' })
+ const uriTemplates = (templates.json.result?.resourceTemplates ?? []).map((t) => t.uriTemplate)
+ assert(uriTemplates.includes('orders://{orderId}'), `templates: ${uriTemplates}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'orders://o-42' }, name: 'orders://o-42' })
+ const record = JSON.parse(read.json.result?.contents?.[0]?.text ?? '{}')
+ assert(record.orderId === 'o-42' && record.status === 'shipped', JSON.stringify(read.json))
+ assert(read.json.result.ttlMs === 60000, `ttlMs ${read.json.result.ttlMs}`)
+ return `orders://o-42 -> ${record.status}, ttlMs=${read.json.result.ttlMs}`
+})
+
+await check('7. prompts list + get', async () => {
+ const list = await request({ method: 'prompts/list' })
+ const names = (list.json.result?.prompts ?? []).map((p) => p.name)
+ assert(names.includes('summarize_order'), `prompts: ${names}`)
+ const got = await request({
+ method: 'prompts/get',
+ params: { name: 'summarize_order', arguments: { orderId: 'o-7' } },
+ name: 'summarize_order',
+ })
+ const text = got.json.result?.messages?.[0]?.content?.text ?? ''
+ assert(text.includes('o-7'), JSON.stringify(got.json))
+ return 'summarize_order fills its argument'
+})
+
+await check('8. server/discover surfaces the instructions', async () => {
+ const r = await request({ method: 'server/discover' })
+ assert((r.json.result?.instructions ?? '').includes('Demo MCP server'), JSON.stringify(r.json.result ?? r.json))
+ return 'instructions present'
+})
+
+await check('9. legacy initialize is answered on the same endpoint', async () => {
+ const r = await request({
+ method: 'initialize',
+ params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'legacy', version: '1.0.0' } },
+ envelope: false,
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ const version = r.json.result?.protocolVersion ?? ''
+ assert(version.startsWith('2025-'), JSON.stringify(r.json))
+ return `served as ${version}`
+})
+
+await check('10. GET is answered with the spec-mandated 405', async () => {
+ let headers = { accept: 'application/json, text/event-stream' }
+ if (signer) headers = await signer('GET', headers, null)
+ const res = await fetch(url, { method: 'GET', headers })
+ assert(res.status === 405, `HTTP ${res.status}`)
+ return 'HTTP 405'
+})
+
+await check('11. Mcp-Method header mismatching the body is rejected (-32020)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'add', arguments: { a: 1, b: 1 } },
+ name: 'add',
+ headerMethod: 'tools/list',
+ })
+ assert(rejectedWith(r.json, -32020), JSON.stringify(r.json))
+ return r.json.error.code === -32020 ? '-32020' : 'rejected (platform-wrapped 400)'
+})
+
+await check('12. elicitation without the client capability is rejected (-32021)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-9' } },
+ name: 'approve_refund',
+ capabilities: {},
+ })
+ assert(rejectedWith(r.json, -32021), JSON.stringify(r.json))
+ return r.json.error.code === -32021 ? '-32021' : 'rejected (platform-wrapped 400)'
+})
+
+if (process.env.LONG) {
+ await check('13. LONG: 45-step stream (~36s) survives past the 29s mark', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 }, _meta: { progressToken: 'long' } },
+ name: 'slow_report',
+ })
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 45, `${progress.length} progress events`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', 'final result missing (stream cut?)')
+ assert(r.ms > 34000, `finished suspiciously fast (${r.ms}ms)`)
+ return `45 events over ${(r.ms / 1000).toFixed(1)}s, final result received`
+ })
+}
+
+if (process.env.LONG) {
+ await check('14. LONG: a silent 36s call is not cut short', async () => {
+ // slow_report emits progress only when given a progressToken, so this call
+ // writes nothing at all for ~36s and then returns plain JSON. That is the
+ // case an edge-optimized API Gateway endpoint kills at 30s (its idle
+ // timeout starts at invoke, not at the first byte, and a raised
+ // timeoutInMillis does not help) - which is why the REST-fronted examples
+ // deploy on a regional endpoint.
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status} after ${(r.ms / 1000).toFixed(1)}s`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', JSON.stringify(r.json).slice(0, 200))
+ return `plain JSON after ${(r.ms / 1000).toFixed(1)}s of silence`
+ })
+}
+
+// --- summary -------------------------------------------------------------------
+
+const failed = results.filter((r) => !r.ok)
+console.log(`\n${results.length - failed.length}/${results.length} passed`)
+process.exit(failed.length ? 1 : 0)
diff --git a/aws-bedrock-agentcore/javascript/mcp-server/index.js b/aws-bedrock-agentcore/javascript/mcp-server/index.js
index 3e6da1303..2ed85bb27 100644
--- a/aws-bedrock-agentcore/javascript/mcp-server/index.js
+++ b/aws-bedrock-agentcore/javascript/mcp-server/index.js
@@ -1,145 +1,30 @@
/**
- * MCP Server for AWS Bedrock AgentCore Runtime
+ * MCP Server for AWS Bedrock AgentCore Runtime.
*
- * A stateless MCP server exposing simple tools via the Model Context Protocol.
- * Uses Express + @modelcontextprotocol/sdk with Streamable HTTP transport.
+ * Serves the canonical aws-mcp-servers example server (src/server.mjs) -
+ * built with the official MCP TypeScript SDK v2, speaking the stateless MCP
+ * 2026-07-28 protocol revision, with the SDK's built-in fallback for older
+ * clients on the same endpoint.
*
- * Tools:
- * - add: Add two numbers
- * - multiply: Multiply two numbers
- * - get_current_time: Get the current date and time
+ * AgentCore Runtime expects the MCP endpoint on port 8000 at /mcp.
+ * toNodeHandler adapts Node's req/res to the SDK's web-standard handler.
+ * The platform adds an Mcp-Session-Id header for its own session isolation;
+ * stateless servers accept and ignore it.
*/
-
-import express from 'express'
-import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
-import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
-import { z } from 'zod'
-
-// ---------------------------------------------------------------------------
-// MCP Server factory — creates a fresh stateless server per request
-// ---------------------------------------------------------------------------
-
-const createMcpServer = () => {
- const server = new McpServer({
- name: 'mcp-server',
- version: '1.0.0',
- })
-
- // Tool: add two numbers
- server.registerTool(
- 'add',
- {
- title: 'Addition',
- description: 'Add two numbers together',
- inputSchema: {
- a: z.number().describe('First number'),
- b: z.number().describe('Second number'),
- },
- },
- async ({ a, b }) => ({
- content: [{ type: 'text', text: String(a + b) }],
- }),
- )
-
- // Tool: multiply two numbers
- server.registerTool(
- 'multiply',
- {
- title: 'Multiplication',
- description: 'Multiply two numbers together',
- inputSchema: {
- a: z.number().describe('First number'),
- b: z.number().describe('Second number'),
- },
- },
- async ({ a, b }) => ({
- content: [{ type: 'text', text: String(a * b) }],
- }),
- )
-
- // Tool: get current time
- server.registerTool(
- 'get_current_time',
- {
- title: 'Current Time',
- description:
- 'Get the current date and time. Optionally specify a timezone.',
- inputSchema: {
- timezone: z
- .string()
- .optional()
- .describe(
- 'Timezone (e.g. "America/New_York", "Europe/London", "UTC")',
- ),
- },
- },
- async ({ timezone }) => {
- const now = new Date()
- const options = {
- timeZone: timezone || 'UTC',
- dateStyle: 'full',
- timeStyle: 'long',
- }
- return {
- content: [{ type: 'text', text: now.toLocaleString('en-US', options) }],
- }
- },
- )
-
- return server
-}
-
-// ---------------------------------------------------------------------------
-// Express HTTP layer — stateless Streamable HTTP on port 8000
-// ---------------------------------------------------------------------------
+import { createServer } from 'node:http'
+import { toNodeHandler } from '@modelcontextprotocol/node'
+import mcp from './src/server.mjs'
const PORT = 8000
-const app = express()
-app.use(express.json())
+const node = toNodeHandler(mcp)
-// POST /mcp — handle MCP JSON-RPC requests (stateless)
-app.post('/mcp', async (req, res) => {
- const server = createMcpServer()
- try {
- const transport = new StreamableHTTPServerTransport({
- sessionIdGenerator: undefined, // stateless — no persistent sessions
- enableJsonResponse: true,
- })
- await server.connect(transport)
- res.on('close', () => {
- transport.close()
- server.close()
- })
- await transport.handleRequest(req, res, req.body)
- } catch (error) {
- console.error('Error handling MCP request:', error)
- if (!res.headersSent) {
- res.status(500).json({
- jsonrpc: '2.0',
- error: {
- code: -32603,
- message: 'Internal server error',
- },
- id: null,
- })
- }
+createServer((req, res) => {
+ if (!req.url?.startsWith('/mcp')) {
+ res.writeHead(404, { 'content-type': 'application/json' })
+ res.end(JSON.stringify({ error: 'not found' }))
+ return
}
-})
-
-// GET /mcp — method not allowed per MCP spec
-app.get('/mcp', (_req, res) => {
- res.writeHead(405).end(
- JSON.stringify({
- jsonrpc: '2.0',
- error: {
- code: -32000,
- message: 'Method not allowed.',
- },
- id: null,
- }),
- )
-})
-
-app.listen(PORT, '0.0.0.0', () => {
+ node(req, res)
+}).listen(PORT, '0.0.0.0', () => {
console.log(`MCP server running on http://0.0.0.0:${PORT}/mcp`)
})
diff --git a/aws-bedrock-agentcore/javascript/mcp-server/package-lock.json b/aws-bedrock-agentcore/javascript/mcp-server/package-lock.json
index 3ebf1af14..08c9f7529 100644
--- a/aws-bedrock-agentcore/javascript/mcp-server/package-lock.json
+++ b/aws-bedrock-agentcore/javascript/mcp-server/package-lock.json
@@ -8,38 +8,52 @@
"name": "mcp-server",
"version": "1.0.0",
"dependencies": {
- "@aws-sdk/client-bedrock-agentcore": "^3.993.0",
- "@modelcontextprotocol/sdk": "^1.24.0",
- "express": "^5.1.0",
+ "@modelcontextprotocol/node": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
"zod": "^4.4.3"
},
+ "devDependencies": {
+ "@aws-crypto/sha256-js": "^5.2.0",
+ "@aws-sdk/credential-provider-node": "^3.750.0",
+ "@smithy/protocol-http": "^5.0.0",
+ "@smithy/signature-v4": "^5.0.0"
+ },
"engines": {
"node": "24.x"
}
},
- "node_modules/@aws-sdk/client-bedrock-agentcore": {
- "version": "3.1085.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agentcore/-/client-bedrock-agentcore-3.1085.0.tgz",
- "integrity": "sha512-IQeQxUxDznQBPNz2vFsCtcaTNTHtticFPZ9dnZywyT6RoQrIKCbRBEIRhTwp4Q2m6mvkEhz/61TtsLFSmyNO7w==",
+ "node_modules/@aws-crypto/sha256-js": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
+ "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.975.1",
- "@aws-sdk/credential-provider-node": "^3.972.66",
- "@aws-sdk/types": "^3.974.0",
- "@smithy/core": "^3.29.2",
- "@smithy/fetch-http-handler": "^5.6.4",
- "@smithy/node-http-handler": "^4.9.4",
- "@smithy/types": "^4.16.0",
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
"tslib": "^2.6.2"
},
"engines": {
- "node": ">=20.0.0"
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/util": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
+ "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.222.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
}
},
"node_modules/@aws-sdk/core": {
"version": "3.975.1",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.975.1.tgz",
"integrity": "sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.974.0",
@@ -59,6 +73,7 @@
"version": "3.972.57",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.57.tgz",
"integrity": "sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
@@ -75,6 +90,7 @@
"version": "3.972.59",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.59.tgz",
"integrity": "sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
@@ -93,6 +109,7 @@
"version": "3.973.1",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.1.tgz",
"integrity": "sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
@@ -117,6 +134,7 @@
"version": "3.972.63",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.63.tgz",
"integrity": "sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
@@ -134,6 +152,7 @@
"version": "3.972.66",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.66.tgz",
"integrity": "sha512-f+qjRXZpz7sgzbc4QB+6nLKfyKFgRRXzWdXbsKPv/VhVRyHsDyq4yBWC/B75BAJpFIcUeI2XR/3gdWJ677zB4A==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/credential-provider-env": "^3.972.57",
@@ -156,6 +175,7 @@
"version": "3.972.57",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.57.tgz",
"integrity": "sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
@@ -172,6 +192,7 @@
"version": "3.973.1",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.1.tgz",
"integrity": "sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
@@ -190,6 +211,7 @@
"version": "3.972.63",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.63.tgz",
"integrity": "sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
@@ -207,6 +229,7 @@
"version": "3.997.31",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.31.tgz",
"integrity": "sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
@@ -226,6 +249,7 @@
"version": "3.996.39",
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.39.tgz",
"integrity": "sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.974.0",
@@ -241,6 +265,7 @@
"version": "3.1083.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1083.0.tgz",
"integrity": "sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
@@ -258,6 +283,7 @@
"version": "3.974.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.0.tgz",
"integrity": "sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.16.0",
@@ -271,6 +297,7 @@
"version": "3.972.34",
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.34.tgz",
"integrity": "sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.16.0",
@@ -284,6 +311,7 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
"integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==",
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
@@ -301,50 +329,57 @@
"hono": "^4"
}
},
- "node_modules/@modelcontextprotocol/sdk": {
- "version": "1.26.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
- "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
+ "node_modules/@modelcontextprotocol/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
"license": "MIT",
"dependencies": {
- "@hono/node-server": "^1.19.9",
- "ajv": "^8.17.1",
- "ajv-formats": "^3.0.1",
- "content-type": "^1.0.5",
- "cors": "^2.8.5",
- "cross-spawn": "^7.0.5",
- "eventsource": "^3.0.2",
- "eventsource-parser": "^3.0.0",
- "express": "^5.2.1",
- "express-rate-limit": "^8.2.1",
- "hono": "^4.11.4",
- "jose": "^6.1.3",
- "json-schema-typed": "^8.0.2",
- "pkce-challenge": "^5.0.0",
- "raw-body": "^3.0.0",
- "zod": "^3.25 || ^4.0",
- "zod-to-json-schema": "^3.25.1"
+ "zod": "^4.2.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/node": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz",
+ "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9"
+ },
+ "engines": {
+ "node": ">=20"
},
"peerDependencies": {
- "@cfworker/json-schema": "^4.1.1",
- "zod": "^3.25 || ^4.0"
+ "@modelcontextprotocol/server": "^2.0.0",
+ "hono": "^4.11.4"
},
"peerDependenciesMeta": {
- "@cfworker/json-schema": {
+ "hono": {
"optional": true
- },
- "zod": {
- "optional": false
}
}
},
+ "node_modules/@modelcontextprotocol/server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/core": "2.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/@smithy/core": {
- "version": "3.29.3",
- "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.3.tgz",
- "integrity": "sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A==",
+ "version": "3.31.1",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.31.1.tgz",
+ "integrity": "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.16.1",
@@ -358,6 +393,7 @@
"version": "4.4.8",
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.8.tgz",
"integrity": "sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.3",
@@ -372,6 +408,7 @@
"version": "5.6.5",
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.5.tgz",
"integrity": "sha512-SuqeisTyPoiIPtIYru/sGxGyXzmZ+8nnFOhC+qRPglt06Ebd1yH//CDltZB2J/3WBNVhwfUaZ0EtHB3cm2X32g==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.3",
@@ -382,10 +419,24 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@smithy/is-array-buffer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
+ "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@smithy/node-http-handler": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.5.tgz",
"integrity": "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.3",
@@ -396,10 +447,25 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@smithy/protocol-http": {
+ "version": "5.5.16",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.5.16.tgz",
+ "integrity": "sha512-iPaxIe9mTyidyhM6WF2/gSLPezyCwSlZcqlDBj2KCoSS994iw4yf1se1xmZkWsY98ZpwjDR/ZMubOSVx+Nrokw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.31.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@smithy/signature-v4": {
"version": "5.6.4",
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.4.tgz",
"integrity": "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.3",
@@ -414,6 +480,7 @@
"version": "4.16.1",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz",
"integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -422,1071 +489,58 @@
"node": ">=18.0.0"
}
},
- "node_modules/accepts": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
- "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
- "license": "MIT",
+ "node_modules/@smithy/util-buffer-from": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
+ "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
+ "dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "mime-types": "^3.0.0",
- "negotiator": "^1.0.0"
+ "@smithy/is-array-buffer": "^2.2.0",
+ "tslib": "^2.6.2"
},
"engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/ajv": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
- "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
+ "node": ">=14.0.0"
}
},
- "node_modules/ajv-formats": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
- "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
- "license": "MIT",
- "dependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependenciesMeta": {
- "ajv": {
- "optional": true
- }
- }
- },
- "node_modules/body-parser": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
- "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
- "license": "MIT",
+ "node_modules/@smithy/util-utf8": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
+ "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
+ "dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "bytes": "^3.1.2",
- "content-type": "^1.0.5",
- "debug": "^4.4.3",
- "http-errors": "^2.0.0",
- "iconv-lite": "^0.7.0",
- "on-finished": "^2.4.1",
- "qs": "^6.14.1",
- "raw-body": "^3.0.1",
- "type-is": "^2.0.1"
+ "@smithy/util-buffer-from": "^2.2.0",
+ "tslib": "^2.6.2"
},
"engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "node": ">=14.0.0"
}
},
"node_modules/bowser": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/content-disposition": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
- "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/content-type": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
- "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
- "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
- "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
- "license": "MIT",
- "engines": {
- "node": ">=6.6.0"
- }
- },
- "node_modules/cors": {
- "version": "2.8.6",
- "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
- "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
- "license": "MIT",
- "dependencies": {
- "object-assign": "^4",
- "vary": "^1"
- },
- "engines": {
- "node": ">= 0.10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
- "license": "MIT"
- },
- "node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
- "license": "MIT"
- },
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/eventsource": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
- "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
- "license": "MIT",
- "dependencies": {
- "eventsource-parser": "^3.0.1"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/eventsource-parser": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
- "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
- "license": "MIT",
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/express": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
- "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
- "license": "MIT",
- "dependencies": {
- "accepts": "^2.0.0",
- "body-parser": "^2.2.1",
- "content-disposition": "^1.0.0",
- "content-type": "^1.0.5",
- "cookie": "^0.7.1",
- "cookie-signature": "^1.2.1",
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "finalhandler": "^2.1.0",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.0",
- "merge-descriptors": "^2.0.0",
- "mime-types": "^3.0.0",
- "on-finished": "^2.4.1",
- "once": "^1.4.0",
- "parseurl": "^1.3.3",
- "proxy-addr": "^2.0.7",
- "qs": "^6.14.0",
- "range-parser": "^1.2.1",
- "router": "^2.2.0",
- "send": "^1.1.0",
- "serve-static": "^2.2.0",
- "statuses": "^2.0.1",
- "type-is": "^2.0.1",
- "vary": "^1.1.2"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/express-rate-limit": {
- "version": "8.5.1",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz",
- "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==",
- "license": "MIT",
- "dependencies": {
- "ip-address": "^10.2.0"
- },
- "engines": {
- "node": ">= 16"
- },
- "funding": {
- "url": "https://github.com/sponsors/express-rate-limit"
- },
- "peerDependencies": {
- "express": ">= 4.11"
- }
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "license": "MIT"
- },
- "node_modules/fast-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
- "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fastify"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fastify"
- }
- ],
- "license": "BSD-3-Clause"
- },
- "node_modules/finalhandler": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
- "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "on-finished": "^2.4.1",
- "parseurl": "^1.3.3",
- "statuses": "^2.0.1"
- },
- "engines": {
- "node": ">= 18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/forwarded": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
- "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fresh": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
- "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/hono": {
"version": "4.12.18",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz",
"integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=16.9.0"
}
},
- "node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
- "license": "MIT",
- "dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/iconv-lite": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
- "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "license": "ISC"
- },
- "node_modules/ip-address": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
- "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
- "license": "MIT",
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/is-promise": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
- "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
- "license": "MIT"
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "license": "ISC"
- },
- "node_modules/jose": {
- "version": "6.1.3",
- "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
- "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/panva"
- }
- },
- "node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "license": "MIT"
- },
- "node_modules/json-schema-typed": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
- "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
- "license": "BSD-2-Clause"
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/media-typer": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
- "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
- "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/mime-db": {
- "version": "1.54.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
- "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
- "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "^1.54.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/negotiator": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
- "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-inspect": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/on-finished": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
- "license": "MIT",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "license": "ISC",
- "dependencies": {
- "wrappy": "1"
- }
- },
- "node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-to-regexp": {
- "version": "8.4.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz",
- "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/pkce-challenge": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
- "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
- "license": "MIT",
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/proxy-addr": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
- "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
- "license": "MIT",
- "dependencies": {
- "forwarded": "0.2.0",
- "ipaddr.js": "1.9.1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/qs": {
- "version": "6.15.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
- "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "side-channel": "^1.1.0"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/raw-body": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
- "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.7.0",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/router": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
- "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "is-promise": "^4.0.0",
- "parseurl": "^1.3.3",
- "path-to-regexp": "^8.0.0"
- },
- "engines": {
- "node": ">= 18"
- }
- },
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "license": "MIT"
- },
- "node_modules/send": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
- "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.3",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.1",
- "mime-types": "^3.0.2",
- "ms": "^2.1.3",
- "on-finished": "^2.4.1",
- "range-parser": "^1.2.1",
- "statuses": "^2.0.2"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/serve-static": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
- "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
- "license": "MIT",
- "dependencies": {
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "parseurl": "^1.3.3",
- "send": "^1.2.0"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/setprototypeof": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
- "license": "ISC"
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
- "side-channel-map": "^1.0.1",
- "side-channel-weakmap": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-map": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-weakmap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3",
- "side-channel-map": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.6"
- }
- },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
"license": "0BSD"
},
- "node_modules/type-is": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
- "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
- "license": "MIT",
- "dependencies": {
- "content-type": "^1.0.5",
- "media-typer": "^1.1.0",
- "mime-types": "^3.0.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/vary": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC"
- },
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
@@ -1495,15 +549,6 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
- },
- "node_modules/zod-to-json-schema": {
- "version": "3.25.1",
- "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
- "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
- "license": "ISC",
- "peerDependencies": {
- "zod": "^3.25 || ^4"
- }
}
}
}
diff --git a/aws-bedrock-agentcore/javascript/mcp-server/package.json b/aws-bedrock-agentcore/javascript/mcp-server/package.json
index c56b2a5a7..56c8ab68c 100644
--- a/aws-bedrock-agentcore/javascript/mcp-server/package.json
+++ b/aws-bedrock-agentcore/javascript/mcp-server/package.json
@@ -8,12 +8,17 @@
"start": "node index.js"
},
"dependencies": {
- "@aws-sdk/client-bedrock-agentcore": "^3.993.0",
- "@modelcontextprotocol/sdk": "^1.24.0",
- "express": "^5.1.0",
+ "@modelcontextprotocol/node": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
"zod": "^4.4.3"
},
"engines": {
"node": "24.x"
+ },
+ "devDependencies": {
+ "@aws-crypto/sha256-js": "^5.2.0",
+ "@aws-sdk/credential-provider-node": "^3.750.0",
+ "@smithy/protocol-http": "^5.0.0",
+ "@smithy/signature-v4": "^5.0.0"
}
-}
+}
\ No newline at end of file
diff --git a/aws-bedrock-agentcore/javascript/mcp-server/serverless.yml b/aws-bedrock-agentcore/javascript/mcp-server/serverless.yml
index b7886e0d7..2c655b7d7 100644
--- a/aws-bedrock-agentcore/javascript/mcp-server/serverless.yml
+++ b/aws-bedrock-agentcore/javascript/mcp-server/serverless.yml
@@ -1,7 +1,9 @@
# MCP Server Example
#
-# A JavaScript MCP server deployed to AWS Bedrock AgentCore Runtime.
-# Exposes tools (add, multiply, get_current_time) via the Model Context Protocol.
+# A JavaScript MCP server deployed to AWS Bedrock AgentCore Runtime, serving
+# the canonical aws-mcp-servers example server: tools (add, slow_report with
+# streamed progress, approve_refund with an elicitation round-trip),
+# resources, a prompt, server/discover instructions, and cache hints.
service: mcp-server
@@ -15,3 +17,20 @@ ai:
agents:
assistant:
protocol: MCP
+ # Forward the MCP standard headers to the container - protocol revision
+ # 2026-07-28 requires Mcp-Method / Mcp-Name on every request, and the
+ # SDK validates them against the body:
+ requestHeaders:
+ allowlist:
+ - Mcp-Method
+ - Mcp-Name
+ - Mcp-Protocol-Version
+
+ # Platform OAuth (config-only): have the runtime validate Bearer JWTs
+ # from any OpenID Connect provider before requests reach the container.
+ # The default (no authorizer) is IAM/SigV4 auth.
+ # authorizer:
+ # jwt:
+ # discoveryUrl: https:///.well-known/openid-configuration
+ # allowedAudience:
+ # - https://
diff --git a/aws-bedrock-agentcore/javascript/mcp-server/src/server.mjs b/aws-bedrock-agentcore/javascript/mcp-server/src/server.mjs
new file mode 100644
index 000000000..0a8c36a1b
--- /dev/null
+++ b/aws-bedrock-agentcore/javascript/mcp-server/src/server.mjs
@@ -0,0 +1,204 @@
+/**
+ * A standard MCP server built with the official MCP TypeScript SDK v2,
+ * serving the stateless MCP 2026-07-28 protocol revision (and answering
+ * older clients through the SDK's built-in fallback on the same endpoint).
+ *
+ * Nothing in this file is Lambda-specific - the same handler runs on any
+ * web-standard host. Every example in mcp/custom/ serves this exact
+ * server; only the hosting glue around it differs.
+ *
+ * Surface:
+ * tools: add (plain JSON), slow_report (progress streaming),
+ * approve_refund (elicitation - pauses to ask the user)
+ * resources: guide://usage, orders://{orderId} (template)
+ * prompts: summarize_order
+ * extras: instructions (served via server/discover), cache hints
+ */
+import {
+ acceptedContent,
+ createMcpHandler,
+ inputRequired,
+ inputResponse,
+ McpServer,
+ ResourceTemplate,
+} from '@modelcontextprotocol/server'
+import { z } from 'zod'
+
+const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
+
+export default createMcpHandler(() => {
+ const server = new McpServer(
+ { name: 'aws-mcp-server', version: '1.0.0' },
+ {
+ // Shown to clients that probe the server/discover method:
+ instructions:
+ 'Demo MCP server. Call add for arithmetic, slow_report to watch ' +
+ 'streamed progress, approve_refund to see a tool ask the user for ' +
+ 'confirmation mid-call. Read guide://usage for a walkthrough.',
+ // Cache hints: let clients and shared caches reuse the tool list for
+ // five minutes. Without hints the SDK emits the conservative defaults
+ // (ttlMs: 0, cacheScope: private) on every cacheable result:
+ cacheHints: { 'tools/list': { ttlMs: 300000, cacheScope: 'public' } },
+ },
+ )
+
+ // A plain tool: zod schemas next to the implementation; returning
+ // structuredContent lets clients consume typed results.
+ server.registerTool(
+ 'add',
+ {
+ description: 'Add two numbers',
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
+ outputSchema: z.object({ sum: z.number() }),
+ },
+ async ({ a, b }) => {
+ const output = { sum: a + b }
+ return {
+ content: [{ type: 'text', text: String(output.sum) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ // A long-running tool emitting progress notifications. When the client
+ // requests progress (a progressToken in _meta) and accepts text/event-stream,
+ // the response is served as SSE with notifications ahead of the final result.
+ server.registerTool(
+ 'slow_report',
+ {
+ description: 'Generate a report, reporting progress along the way',
+ inputSchema: z.object({ steps: z.number().default(3) }),
+ },
+ async ({ steps }, ctx) => {
+ // Real clients request progress via `_meta.progressToken`, never as a
+ // tool argument.
+ const progressToken = ctx.mcpReq._meta?.progressToken
+ for (let i = 1; i <= steps; i++) {
+ await sleep(800)
+ if (progressToken) {
+ await ctx.mcpReq.notify({
+ method: 'notifications/progress',
+ params: { progressToken, progress: i, total: steps, message: `step ${i}` },
+ })
+ }
+ }
+ return { content: [{ type: 'text', text: `completed ${steps} steps` }] }
+ },
+ )
+
+ // Elicitation - a tool that pauses to ask the user for input. The handler
+ // returns an input_required result; the client asks the user and retries
+ // the call with the answers attached, re-entering the handler, which reads
+ // them with acceptedContent. Works statelessly - no session required.
+ server.registerTool(
+ 'approve_refund',
+ {
+ description: 'Refund an order after user confirmation',
+ inputSchema: z.object({ orderId: z.string() }),
+ },
+ async ({ orderId }, ctx) => {
+ const answer = acceptedContent(
+ ctx.mcpReq.inputResponses,
+ 'confirm',
+ z.object({ confirmed: z.boolean() }),
+ )
+ // Decline/cancel must terminate: acceptedContent returns undefined for
+ // BOTH "not asked yet" and "user said no" - without this check a decline
+ // falls through to inputRequired and the client re-prompts forever.
+ const confirmView = inputResponse(ctx.mcpReq.inputResponses, 'confirm')
+ if (confirmView.kind === 'elicit' && confirmView.action !== 'accept') {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ if (answer === undefined) {
+ return inputRequired({
+ inputRequests: {
+ confirm: inputRequired.elicit({
+ message: `Refund order ${orderId}?`,
+ requestedSchema: z.object({ confirmed: z.boolean() }),
+ }),
+ },
+ })
+ }
+ if (!answer.confirmed) {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ return { content: [{ type: 'text', text: `refunded ${orderId}` }] }
+ },
+ )
+
+ // A readable document next to the tools. Clients fetch it with
+ // resources/read (the Mcp-Name header carries the uri).
+ server.registerResource(
+ 'usage-guide',
+ 'guide://usage',
+ { description: 'How to use this server', mimeType: 'text/markdown' },
+ async (uri) => ({
+ contents: [
+ {
+ uri: uri.href,
+ text: '# Usage\n\nCall `add` for sums, `slow_report` for streamed progress, `approve_refund` for an elicitation round-trip.',
+ },
+ ],
+ }),
+ )
+
+ // A resource template - parameterized URIs resolved per request. The
+ // per-resource cacheHint overrides the server-level hints for its reads.
+ server.registerResource(
+ 'order',
+ new ResourceTemplate('orders://{orderId}', { list: undefined }),
+ { description: 'Order record by id', cacheHint: { ttlMs: 60000, cacheScope: 'private' } },
+ async (uri, { orderId }) => ({
+ contents: [{ uri: uri.href, text: JSON.stringify({ orderId, status: 'shipped' }) }],
+ }),
+ )
+
+ // A prompt template clients can list and fill in.
+ server.registerPrompt(
+ 'summarize_order',
+ {
+ description: 'Ask the model to summarize an order',
+ argsSchema: z.object({ orderId: z.string() }),
+ },
+ ({ orderId }) => ({
+ messages: [
+ {
+ role: 'user',
+ content: { type: 'text', text: `Summarize the status of order ${orderId} in one sentence.` },
+ },
+ ],
+ }),
+ )
+
+ return server
+})
+
+// ---------------------------------------------------------------------------
+// Going further (uncomment and adapt):
+// ---------------------------------------------------------------------------
+//
+// Signed round-trip state - when an elicitation round-trip must carry server
+// state the retry cannot be allowed to tamper with (a computed price, an
+// idempotency key), return it as requestState sealed with the SDK's HMAC
+// codec. Store the >= 32-byte key in Secrets Manager or SSM:
+//
+// import { createRequestStateCodec } from '@modelcontextprotocol/server'
+// const codec = createRequestStateCodec({ key: process.env.MCP_STATE_KEY, ttlSeconds: 600 })
+//
+// In-process OAuth (works identically behind every front door): the SDK ships
+// bearer-token middleware and OAuth discovery-document helpers. Wrap the
+// handler's fetch face before exporting - any OpenID Connect provider works:
+//
+// import { requireBearerAuth, oauthMetadataResponse } from '@modelcontextprotocol/server'
+// import { createRemoteJWKSet, jwtVerify } from 'jose'
+//
+// const jwks = createRemoteJWKSet(new URL('https:///.well-known/jwks.json'))
+// const verifier = {
+// async verifyAccessToken(token) {
+// const { payload } = await jwtVerify(token, jwks, {
+// issuer: 'https:///',
+// audience: 'https://',
+// })
+// return { token, clientId: payload.azp, scopes: payload.scope?.split(' ') ?? [], expiresAt: payload.exp }
+// },
+// }
diff --git a/aws-bedrock-agentcore/javascript/mcp-server/test-invoke.js b/aws-bedrock-agentcore/javascript/mcp-server/test-invoke.js
deleted file mode 100644
index 68a3591d1..000000000
--- a/aws-bedrock-agentcore/javascript/mcp-server/test-invoke.js
+++ /dev/null
@@ -1,153 +0,0 @@
-#!/usr/bin/env node
-/**
- * Test script to invoke the MCP Server deployed to AgentCore Runtime.
- *
- * Usage:
- * RUNTIME_ARN=arn:aws:bedrock-agentcore:... node test-invoke.js
- *
- * Or set RUNTIME_ARN environment variable before running.
- *
- * Requires: @aws-sdk/client-bedrock-agentcore
- * npm install @aws-sdk/client-bedrock-agentcore
- */
-
-import {
- BedrockAgentCoreClient,
- InvokeAgentRuntimeCommand,
-} from '@aws-sdk/client-bedrock-agentcore'
-
-const RUNTIME_ARN = process.env.RUNTIME_ARN
-const REGION = process.env.AWS_REGION || 'us-east-1'
-
-if (!RUNTIME_ARN) {
- console.error('Error: RUNTIME_ARN environment variable is required.')
- console.error('Usage: RUNTIME_ARN= node test-invoke.js')
- console.error('\nGet your runtime ARN from: serverless info')
- process.exit(1)
-}
-
-const client = new BedrockAgentCoreClient({ region: REGION })
-let sessionId = null
-
-async function invoke(method, params, id) {
- const payload = { jsonrpc: '2.0', method, id }
- if (params) payload.params = params
-
- const command = new InvokeAgentRuntimeCommand({
- agentRuntimeArn: RUNTIME_ARN,
- qualifier: 'DEFAULT',
- ...(sessionId ? { runtimeSessionId: sessionId } : {}),
- payload: Buffer.from(JSON.stringify(payload)),
- contentType: 'application/json',
- accept: 'application/json, text/event-stream',
- })
-
- const response = await client.send(command)
-
- // Capture session ID from first response
- if (!sessionId && response.runtimeSessionId) {
- sessionId = response.runtimeSessionId
- }
-
- // Parse response body
- const body =
- typeof response.response === 'string'
- ? response.response
- : await streamToString(response.response)
-
- return JSON.parse(body)
-}
-
-async function streamToString(stream) {
- if (typeof stream === 'string') return stream
- if (stream instanceof Uint8Array || Buffer.isBuffer(stream)) {
- return new TextDecoder().decode(stream)
- }
- // Handle ReadableStream or async iterator
- const chunks = []
- for await (const chunk of stream) {
- chunks.push(
- typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk),
- )
- }
- return chunks.join('')
-}
-
-async function testInitialize() {
- console.log('=== Initialize ===')
- const result = await invoke(
- 'initialize',
- {
- protocolVersion: '2025-11-25',
- clientInfo: { name: 'test-client', version: '1.0.0' },
- capabilities: {},
- },
- 1,
- )
- console.log(
- 'Server:',
- result.result?.serverInfo?.name,
- result.result?.serverInfo?.version,
- )
- console.log('Protocol:', result.result?.protocolVersion)
- console.log()
-}
-
-async function testListTools() {
- console.log('=== tools/list ===')
- const result = await invoke('tools/list', undefined, 2)
- const tools = result.result?.tools || []
- for (const tool of tools) {
- console.log(` - ${tool.name}: ${tool.description}`)
- }
- console.log()
-}
-
-async function testCallTool(name, args, id) {
- console.log(`=== tools/call: ${name}(${JSON.stringify(args)}) ===`)
- const result = await invoke('tools/call', { name, arguments: args }, id)
- const content =
- result.result?.content?.[0]?.text ?? JSON.stringify(result.result)
- console.log(` Result: ${content}`)
- console.log()
- return content
-}
-
-async function main() {
- console.log('MCP Server Test Suite')
- console.log(`Runtime ARN: ${RUNTIME_ARN}`)
- console.log(`Region: ${REGION}`)
- console.log('='.repeat(50) + '\n')
-
- // Step 1: Initialize
- await testInitialize()
-
- // Step 2: Send initialized notification
- const notifPayload = { jsonrpc: '2.0', method: 'notifications/initialized' }
- const notifCommand = new InvokeAgentRuntimeCommand({
- agentRuntimeArn: RUNTIME_ARN,
- qualifier: 'DEFAULT',
- runtimeSessionId: sessionId,
- payload: Buffer.from(JSON.stringify(notifPayload)),
- contentType: 'application/json',
- accept: 'application/json, text/event-stream',
- })
- await client.send(notifCommand)
-
- // Step 3: List tools
- await testListTools()
-
- // Step 4: Call each tool
- await testCallTool('add', { a: 5, b: 3 }, 10)
- await testCallTool('multiply', { a: 7, b: 6 }, 11)
- await testCallTool('get_current_time', { timezone: 'UTC' }, 12)
- await testCallTool('get_current_time', { timezone: 'America/New_York' }, 13)
-
- console.log('='.repeat(50))
- console.log('All tests completed!')
-}
-
-main().catch((err) => {
- console.error('Test failed:', err)
- process.exit(1)
-})
diff --git a/examples.json b/examples.json
index 2e9bb402a..d7575bc47 100644
--- a/examples.json
+++ b/examples.json
@@ -289,6 +289,66 @@
"authorName": "Rupak Ganguly",
"authorAvatar": "https://avatars0.githubusercontent.com/u/8188?v=4&s=140"
},
+ {
+ "title": "AWS MCP Server behind API Gateway REST (NodeJS)",
+ "name": "mcp-custom-rest-api",
+ "description": "Run an MCP server built with the official MCP TypeScript SDK on AWS Lambda, with streaming responses through API Gateway REST response streaming.",
+ "githubUrl": "https://github.com/serverless/examples/tree/v4/mcp/custom/rest-api",
+ "framework": "v4",
+ "language": "node",
+ "platform": "aws",
+ "authorLink": "https://github.com/serverless",
+ "authorName": "Serverless, Inc.",
+ "authorAvatar": "https://avatars1.githubusercontent.com/u/13742415?s=200&v=4"
+ },
+ {
+ "title": "AWS MCP Server on Lambda Function URL (NodeJS)",
+ "name": "mcp-custom-function-url",
+ "description": "Run an MCP server built with the official MCP TypeScript SDK on a Lambda Function URL with response streaming - no API Gateway.",
+ "githubUrl": "https://github.com/serverless/examples/tree/v4/mcp/custom/function-url",
+ "framework": "v4",
+ "language": "node",
+ "platform": "aws",
+ "authorLink": "https://github.com/serverless",
+ "authorName": "Serverless, Inc.",
+ "authorAvatar": "https://avatars1.githubusercontent.com/u/13742415?s=200&v=4"
+ },
+ {
+ "title": "AWS MCP Server with Hono (NodeJS)",
+ "name": "mcp-custom-hono",
+ "description": "Run an MCP server on AWS Lambda with the official MCP Hono integration and Hono's aws-lambda adapter - no hand-written Lambda glue at all.",
+ "githubUrl": "https://github.com/serverless/examples/tree/v4/mcp/custom/hono",
+ "framework": "v4",
+ "language": "node",
+ "platform": "aws",
+ "authorLink": "https://github.com/serverless",
+ "authorName": "Serverless, Inc.",
+ "authorAvatar": "https://avatars1.githubusercontent.com/u/13742415?s=200&v=4"
+ },
+ {
+ "title": "AWS MCP Server with Express and Lambda Web Adapter (NodeJS)",
+ "name": "mcp-custom-express-web-adapter",
+ "description": "Run an MCP server as a plain Express app on AWS Lambda with Lambda Web Adapter - zip deployment, streaming through API Gateway REST.",
+ "githubUrl": "https://github.com/serverless/examples/tree/v4/mcp/custom/express-web-adapter",
+ "framework": "v4",
+ "language": "node",
+ "platform": "aws",
+ "authorLink": "https://github.com/serverless",
+ "authorName": "Serverless, Inc.",
+ "authorAvatar": "https://avatars1.githubusercontent.com/u/13742415?s=200&v=4"
+ },
+ {
+ "title": "AWS MCP Server, minimal (NodeJS)",
+ "name": "mcp-minimal",
+ "description": "Deploy an MCP server built with the official MCP TypeScript SDK to AWS Lambda with the Serverless Framework - one module, one line of configuration.",
+ "githubUrl": "https://github.com/serverless/examples/tree/v4/mcp/minimal",
+ "framework": "v4",
+ "language": "node",
+ "platform": "aws",
+ "authorLink": "https://github.com/serverless",
+ "authorName": "Serverless, Inc.",
+ "authorAvatar": "https://avatars1.githubusercontent.com/u/13742415?s=200&v=4"
+ },
{
"title": "Node.js AWS Lambda connecting to MongoDB Atlas",
"name": "aws-node-mongodb-atlas",
@@ -1293,6 +1353,18 @@
"authorName": "Serverless, inc.",
"authorAvatar": "https://avatars1.githubusercontent.com/u/13742415?s=200&v=4"
},
+ {
+ "title": "Bedrock AgentCore: MCP Server from Plain Lambda Functions (JavaScript)",
+ "name": "aws-bedrock-agentcore-javascript-mcp-server-lambda-tools",
+ "description": "MCP server whose tools are plain Lambda functions - no MCP SDK in your code - using Bedrock AgentCore Gateway.",
+ "githubUrl": "https://github.com/serverless/examples/tree/v4/aws-bedrock-agentcore/javascript/mcp-server-lambda-tools",
+ "framework": "v4",
+ "language": "node",
+ "platform": "aws",
+ "authorLink": "https://github.com/serverless",
+ "authorName": "Serverless, Inc.",
+ "authorAvatar": "https://avatars1.githubusercontent.com/u/13742415?s=200&v=4"
+ },
{
"title": "Bedrock AgentCore: Strands Agent with Browser (JavaScript)",
"name": "aws-bedrock-agentcore-javascript-strands-browser",
@@ -2632,5 +2704,41 @@
"authorLink": "https://github.com/eahefnawy",
"authorName": "Eslam λ Hefnawy",
"authorAvatar": "https://avatars.githubusercontent.com/u/2312463?v=4"
+ },
+ {
+ "title": "AWS MCP Server with Amazon Cognito OAuth (NodeJS)",
+ "name": "mcp-oauth-cognito",
+ "description": "Protect an MCP server with an Amazon Cognito user pool validated by API Gateway - zero token-verification code, a self-contained pool and app client, machine-to-machine tokens, with the Serverless Framework.",
+ "githubUrl": "https://github.com/serverless/examples/tree/v4/mcp/oauth-cognito",
+ "framework": "v4",
+ "language": "node",
+ "platform": "aws",
+ "authorLink": "https://github.com/serverless",
+ "authorName": "Serverless, Inc.",
+ "authorAvatar": "https://avatars1.githubusercontent.com/u/13742415?s=200&v=4"
+ },
+ {
+ "title": "AWS MCP Server with a Custom Lambda Authorizer and Auth0 (NodeJS)",
+ "name": "mcp-oauth-authorizer",
+ "description": "Reject unauthorized MCP traffic at API Gateway before the Lambda runs, using your own Lambda authorizer with Auth0 as the identity provider, with the Serverless Framework.",
+ "githubUrl": "https://github.com/serverless/examples/tree/v4/mcp/oauth-authorizer",
+ "framework": "v4",
+ "language": "node",
+ "platform": "aws",
+ "authorLink": "https://github.com/serverless",
+ "authorName": "Serverless, Inc.",
+ "authorAvatar": "https://avatars1.githubusercontent.com/u/13742415?s=200&v=4"
+ },
+ {
+ "title": "AWS MCP Server Verifying OAuth In-Module (NodeJS)",
+ "name": "mcp-oauth-in-module",
+ "description": "Verify OAuth 2.1 bearer tokens inside your MCP server module with the MCP SDK's requireBearerAuth gate - spec-shaped 401 challenges, scope-aware 403s, and the caller's identity in your tools, with the Serverless Framework.",
+ "githubUrl": "https://github.com/serverless/examples/tree/v4/mcp/oauth-in-module",
+ "framework": "v4",
+ "language": "node",
+ "platform": "aws",
+ "authorLink": "https://github.com/serverless",
+ "authorName": "Serverless, Inc.",
+ "authorAvatar": "https://avatars1.githubusercontent.com/u/13742415?s=200&v=4"
}
]
diff --git a/mcp/README.md b/mcp/README.md
new file mode 100644
index 000000000..3717ba19c
--- /dev/null
+++ b/mcp/README.md
@@ -0,0 +1,71 @@
+# MCP Servers on AWS
+
+Host a [Model Context Protocol](https://modelcontextprotocol.io) server on AWS with the Serverless Framework. The MCP `2026-07-28` revision made servers **stateless** — every request is self-contained, so any Lambda instance can serve any request — which makes serverless hosting a natural fit.
+
+**Start with the Framework's [built-in MCP server support](https://www.serverless.com/framework/docs/providers/aws/guide/mcp).** You write a standard MCP SDK module; the Framework owns the endpoint, streaming, the Lambda entry, packaging, the authorizer wiring, the OAuth discovery document, and the elicitation signing key. What it never owns is token verification — [enforcement is yours](https://www.serverless.com/framework/docs/providers/aws/guide/mcp#authentication) — so the examples are organized by **where you put it**:
+
+| Example | Where enforcement lives | Reach for it when |
+| --- | --- | --- |
+| [minimal](minimal) | Nowhere — the endpoint is public | Public tools, or you're just starting |
+| [oauth-cognito](oauth-cognito) | API Gateway, validating a Cognito user pool's tokens itself | OAuth with zero verification code — rejection costs no invocation anywhere |
+| [oauth-authorizer](oauth-authorizer) | Your own Lambda authorizer, consulted before the server is invoked (Auth0 here) | Any other identity provider, or accept/reject logic of your own |
+| [oauth-in-module](oauth-in-module) | Inside your module, with the MCP SDK's own bearer gate | The spec's semantics: `WWW-Authenticate` challenges, scope-aware `403`s, the caller's identity in your tools |
+
+The gateway shapes and the in-module gate compose — cheap rejection in front, the spec's judgement behind it — and interactive browser login (Claude Code discovering the server and logging in itself) is a section of the [oauth-cognito README](oauth-cognito/README.md#interactive-clients-need-a-custom-domain): it needs a root-mapped custom domain, not a different example.
+
+For a different front door, your own bridge code, or per-function control the property does not expose (VPC, layers, provisioned concurrency, a per-function role), see **[custom](custom)** — the same server hosted by hand, one directory per approach.
+
+## Which client can do what
+
+The single most useful thing to know before testing: **not every client reaches every feature**, and the gaps are about the client, not your server. On this hosting (stateless, per-request), the server can only send the client a request — for elicitation, sampling, or roots — when the client speaks the 2026-07-28 revision. Everything request/response works everywhere.
+
+| Client | Tools · resources · prompts | Streamed progress | Elicitation / sampling / roots |
+| --- | --- | --- | --- |
+| `curl` (raw JSON-RPC) | ✅ | ✅ (read the SSE frames) | ✅ (you hand-write the retry envelope) |
+| Official SDK client, default | ✅ | ✅ | ❌ negotiates a legacy revision |
+| Official SDK client, `versionNegotiation: { mode: 'auto' }` | ✅ | ✅ | ✅ |
+| [MCP Inspector](https://github.com/modelcontextprotocol/inspector) v2, CLI mode (`--cli`) | ✅ | ✅ | ❌ never declares the capability, even with `"protocolEra": "modern"` |
+| MCP Inspector v2, browser UI | ✅ | ✅ | ✅ with the connection's **Protocol Era** set to Modern |
+| Claude Code CLI | ✅ | ✅ (renders in the terminal) | ✅ only with the 2026-07-28 rollout enabled |
+
+The reason elicitation is gated: on older revisions it is a wire request the server sends *to* the client, and per-request Lambda serving cannot hold a connection open to receive the answer. The 2026-07-28 revision carries the whole exchange in-band (the tool returns `input_required`; the client answers and retries), which survives statelessness — but a client has to opt into that revision. None of the four examples above registers an elicitation tool; the end-to-end walkthrough — a tool pausing for confirmation, sealed state signed with `state: true` — lives in the [MCP guide's Elicitation state chapter](https://www.serverless.com/framework/docs/providers/aws/guide/mcp#elicitation-state), the [custom](custom) family's shared server carries a runnable `approve_refund`, and the script below shows the client side of answering one.
+
+## Testing any example
+
+Every example's README shows the failure shapes and the working calls as real request/response output; the minimal example also walks through the MCP Inspector, and oauth-cognito carries the Claude Code login walkthrough. The runnable official-client script below works against any deployed endpoint — set `ENDPOINT`, and `BEARER` if the server enforces bearer tokens:
+
+```js
+// client.mjs — official MCP SDK client, opted into the modern revision
+import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'
+
+const client = new Client(
+ { name: 'example-client', version: '1.0.0' },
+ {
+ capabilities: { elicitation: { form: {} } },
+ versionNegotiation: { mode: 'auto' }, // reach elicitation; omit for legacy
+ },
+)
+// A registered handler answers the server's mid-tool questions locally.
+client.setRequestHandler('elicitation/create', async () => ({
+ action: 'accept',
+ content: { confirmed: true },
+}))
+
+const url = new URL(process.env.ENDPOINT)
+const transport = new StreamableHTTPClientTransport(url, {
+ requestInit: process.env.BEARER
+ ? { headers: { authorization: `Bearer ${process.env.BEARER}` } }
+ : undefined,
+})
+
+await client.connect(transport)
+console.log('era', client.getProtocolEra())
+console.log('tools', (await client.listTools()).tools.map((t) => t.name))
+console.log('add', (await client.callTool({ name: 'add', arguments: { a: 2, b: 40 } })).structuredContent)
+await client.close()
+```
+
+```bash
+npm init -y && npm install @modelcontextprotocol/client
+ENDPOINT="https://…/demo/mcp" node client.mjs
+```
diff --git a/mcp/custom/README.md b/mcp/custom/README.md
new file mode 100644
index 000000000..4ffea22de
--- /dev/null
+++ b/mcp/custom/README.md
@@ -0,0 +1,81 @@
+# MCP Servers on AWS
+
+Examples for hosting a [Model Context Protocol](https://modelcontextprotocol.io) server on AWS with the Serverless Framework.
+
+The MCP `2026-07-28` protocol revision made servers **stateless**: every request is self-contained - no initialize handshake, no sessions, no sticky routing - so any request can be served by any Lambda execution environment or container instance. That makes serverless hosting a natural fit, and these examples show every way to do it.
+
+**They all serve the same canonical MCP server** (built with the official [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) v2) and differ only in the hosting glue, so you can compare approaches directly and test every one with the same client script (`client.mjs`, shipped in each example). The canonical server exercises the full protocol surface:
+
+- **Tools** - `add` (plain JSON + structured output), `slow_report` (progress notifications streamed over SSE), `approve_refund` (elicitation: the tool pauses mid-call to ask the user for confirmation and resumes on retry)
+- **Resources** - a static document plus an `orders://{orderId}` template
+- **Prompts** - a fill-in prompt template
+- **`server/discover`** - server `instructions` for client discovery
+- **Cache hints** - `ttlMs`/`cacheScope` on list results, configured rather than the conservative defaults
+- **Legacy fallback** - older clients sending `initialize` are answered on the same endpoint
+- Plus commented extensions in each example: authentication (three flavors, see below) and HMAC-sealed round-trip state
+
+## The examples
+
+| Example | Hosting | Lambda glue | What it uniquely shows |
+| --- | --- | --- | --- |
+| [rest-api](rest-api) | API Gateway REST, `transferMode: stream` | ~80-line adapter (`src/lambda.mjs`) | The full-featured front door: custom domains, WAF, throttling, usage plans, gateway authorizers |
+| [function-url](function-url) | Lambda Function URL, `invokeMode: RESPONSE_STREAM` | ~80-line adapter (Function URL event shape) | The leanest hosting: no API Gateway, no per-request fee, streams bounded only by function timeout |
+| [hono](hono) | Lambda Function URL | **none** - official `@modelcontextprotocol/hono` + Hono's `streamHandle` | Zero custom glue: existing maintained packages do all the bridging |
+| [express-web-adapter](express-web-adapter) | API Gateway REST stream + Lambda Web Adapter (**zip**) | **none** - the app is a plain Express server | Official `@modelcontextprotocol/express`; app code runs unchanged anywhere |
+| [fastify-container](fastify-container) | Function URL + Lambda Web Adapter (**container image**) | **none** - the app is a plain Fastify server | Official `@modelcontextprotocol/fastify`; a portable image that also runs on Fargate/App Runner |
+| [agentcore-runtime](agentcore-runtime) | AWS Bedrock AgentCore Runtime (managed container) | none - plain HTTP container | Managed MCP hosting: session isolation, config-only platform OAuth |
+| [agentcore-gateway](agentcore-gateway) | AWS Bedrock AgentCore Gateway | none - **and no MCP SDK either** | AWS runs the MCP server; your code is plain Lambda functions with the schema in `serverless.yml` |
+
+## Three independent choices
+
+The Lambda-based examples vary along three axes that are **fully independent** - each example pins one combination to stay minimal, not because the pieces require each other:
+
+1. **Web framework / bridge** - hand-written adapter, Hono, Express, or Fastify. Any of them works with any packaging and any front door.
+2. **Packaging** - zip (no Docker needed) or container image (portable beyond Lambda). Express runs fine in a container; Fastify runs fine in a zip.
+3. **Front door** - API Gateway REST in stream mode, or a Lambda Function URL. Both carry plain JSON and SSE on the same endpoint; both were exercised with every bridge.
+
+## Choosing a front door
+
+| | API Gateway REST (stream) | Lambda Function URL |
+| --- | --- | --- |
+| Custom domain, WAF, throttling, usage plans | yes | domain via CloudFront in front |
+| Reject unauthenticated requests before invoking | yes (authorizer) | IAM/SigV4 only |
+| Streaming (SSE) | yes - raise `timeoutInMillis` alongside (up to 15 min), and keep the endpoint regional: edge-optimized endpoints cut streams idle for 30 s | yes - function `timeout` is the only bound |
+| Per-request cost | per-request fee (see [API Gateway pricing](https://aws.amazon.com/api-gateway/pricing/)), no streaming surcharge | none (Lambda streaming meters egress beyond the first 6 MB per response - see [Lambda pricing](https://aws.amazon.com/lambda/pricing/)) |
+
+AgentCore Runtime replaces the front-door question entirely (the platform hosts the endpoint, IAM or JWT auth, consumption-based pricing - see [AgentCore pricing](https://aws.amazon.com/bedrock/agentcore/pricing/)); AgentCore Gateway additionally replaces the MCP server itself.
+
+## Capabilities and limitations
+
+| | Lambda examples (any bridge) | AgentCore Runtime | AgentCore Gateway |
+| --- | --- | --- | --- |
+| Streamed progress (SSE) | yes | yes | not from Lambda-backed tools |
+| Elicitation (tool asks the user) | yes | yes | not from Lambda-backed tools |
+| Exact tool names | yes | yes | prefixed `target___tool` |
+| Cache hints (`ttlMs`/`cacheScope`) | yes (configurable) | yes (configurable) | fixed by the platform |
+| Max tool duration | 15 min (function timeout) | platform limits | synchronous Lambda invoke |
+| Your code contains MCP | the server (official SDK) | the server (official SDK) | nothing - plain functions |
+| Semantic tool search | - | - | yes (built-in search tool) |
+
+## Authentication
+
+Every example includes the same three options (commented, with complete code):
+
+1. **Platform auth** - reject requests before they reach your code: an API Gateway JWT authorizer (`src/authorizer.mjs` in the REST-fronted examples - works with any OpenID Connect provider), `authorizer: aws_iam` on Function URLs, or AgentCore's config-only `authorizer.jwt`.
+2. **In-process** - the SDK's `requireBearerAuth` with a `jose` JWKS verifier; identical behind every front door.
+3. **OAuth discovery** - a `/.well-known/oauth-protected-resource` route so MCP clients can find your authorization server.
+
+## Testing
+
+Each example ships the identical `client.mjs`:
+
+```bash
+ENDPOINT= node client.mjs # 12 protocol checks
+LONG=1 ENDPOINT= node client.mjs # + a ~36s streaming case
+AUTH=sigv4 SERVICE=bedrock-agentcore ENDPOINT= node client.mjs # AgentCore Runtime (SigV4)
+AUTH=sigv4 SERVICE=lambda ENDPOINT= node client.mjs # IAM-auth Function URL
+```
+
+The checks cover the whole surface, including two negative cases proving the `2026-07-28` protocol validation is active end to end: a mismatching `Mcp-Method` header (`-32020`) and an elicitation call from a client that did not declare the capability (`-32021`). `LONG=1` adds two long-call cases - one streaming ~36 s of progress events, one staying completely silent for ~36 s before returning - because those two fail for different reasons on a misconfigured front door.
+
+The canonical copies of the shared files live in this directory ([client.mjs](client.mjs), [server.mjs](server.mjs)); every example carries an identical copy so each directory stays self-contained. Edit the canonical, re-copy into the examples - `node validate.js` at the repo root fails on any drift.
diff --git a/mcp/custom/agentcore-gateway/README.md b/mcp/custom/agentcore-gateway/README.md
new file mode 100644
index 000000000..0b8a80e0e
--- /dev/null
+++ b/mcp/custom/agentcore-gateway/README.md
@@ -0,0 +1,5 @@
+# MCP Server from Plain Lambda Functions (AgentCore Gateway)
+
+This example lives at [`aws-bedrock-agentcore/javascript/mcp-server-lambda-tools`](../../aws-bedrock-agentcore/javascript/mcp-server-lambda-tools) (kept with the AgentCore example family).
+
+It inverts the trade the other examples in this directory make: AWS runs the MCP server (Bedrock AgentCore Gateway) and your code shrinks to plain Lambda functions, with the tool schema declared in `serverless.yml` - no MCP SDK in your code at all. The trade-offs (tool naming, no streaming or elicitation from Lambda-backed tools) are covered in the [comparison table](../README.md).
diff --git a/mcp/custom/agentcore-runtime/README.md b/mcp/custom/agentcore-runtime/README.md
new file mode 100644
index 000000000..0c3c8bf05
--- /dev/null
+++ b/mcp/custom/agentcore-runtime/README.md
@@ -0,0 +1,5 @@
+# MCP Server on AgentCore Runtime
+
+This example lives at [`aws-bedrock-agentcore/javascript/mcp-server`](../../aws-bedrock-agentcore/javascript/mcp-server) (kept with the AgentCore example family).
+
+It serves the same canonical MCP server as the examples in this directory, hosted on AWS Bedrock AgentCore Runtime: the server runs as a plain HTTP container - no Lambda adapter at all - with managed scaling, session isolation, and config-only platform OAuth (`authorizer.jwt`). See the [comparison table](../README.md) for how it stacks up against the Lambda-based options.
diff --git a/mcp/custom/client.mjs b/mcp/custom/client.mjs
new file mode 100644
index 000000000..7ea0a3700
--- /dev/null
+++ b/mcp/custom/client.mjs
@@ -0,0 +1,348 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the mcp/custom examples. Every example serves
+ * the same canonical MCP server, so this same script tests all of them.
+ *
+ * Usage:
+ * ENDPOINT= node client.mjs
+ * LONG=1 ENDPOINT=... node client.mjs # adds a ~36s streaming case
+ * AUTH=sigv4 SERVICE=bedrock-agentcore ENDPOINT= node client.mjs
+ * AUTH=sigv4 SERVICE=lambda ENDPOINT=... node client.mjs # IAM-auth Function URL
+ *
+ * Exercises the full 2026-07-28 surface: plain JSON, SSE progress streaming,
+ * an elicitation round-trip, resources (static + template), prompts,
+ * server/discover, cache hints, the legacy fallback, and two negative cases
+ * (header mismatch -32020, missing capability -32021).
+ */
+
+const ENDPOINT = process.env.ENDPOINT
+if (!ENDPOINT) {
+ console.error('Set ENDPOINT to the MCP endpoint URL')
+ process.exit(1)
+}
+
+// --- endpoint + optional SigV4 signing --------------------------------------
+
+let url = new URL(ENDPOINT)
+let signer
+if (process.env.AUTH === 'sigv4') {
+ const service = process.env.SERVICE || 'lambda'
+ if (service === 'bedrock-agentcore' && url.pathname.includes('/runtimes/')) {
+ // AgentCore runtime URLs embed the agent ARN - the WHOLE ARN must be
+ // URL-encoded in the path (including its inner "/").
+ const arn = decodeURIComponent(url.pathname.match(/\/runtimes\/(.+?)\/invocations/)[1])
+ url = new URL(`${url.origin}/runtimes/${encodeURIComponent(arn)}/invocations`)
+ url.searchParams.set('qualifier', 'DEFAULT')
+ }
+ const [{ SignatureV4 }, { HttpRequest }, { Sha256 }, { defaultProvider }] = await Promise.all([
+ import('@smithy/signature-v4'),
+ import('@smithy/protocol-http'),
+ import('@aws-crypto/sha256-js'),
+ import('@aws-sdk/credential-provider-node'),
+ ])
+ const sigv4 = new SignatureV4({
+ service,
+ region: process.env.AWS_REGION || 'us-east-1',
+ credentials: defaultProvider(),
+ sha256: Sha256,
+ })
+ signer = async (method, headers, body) => {
+ const request = new HttpRequest({
+ method,
+ protocol: url.protocol,
+ hostname: url.hostname,
+ path: url.pathname,
+ query: Object.fromEntries(url.searchParams),
+ headers: { host: url.hostname, ...headers },
+ ...(body != null && { body }),
+ })
+ return (await sigv4.sign(request)).headers
+ }
+}
+
+// --- request helpers ---------------------------------------------------------
+
+const CAPS_ELICIT = { elicitation: { form: {} } }
+const meta = (capabilities = {}) => ({
+ 'io.modelcontextprotocol/protocolVersion': '2026-07-28',
+ 'io.modelcontextprotocol/clientInfo': { name: 'aws-mcp-servers-client', version: '1.0.0' },
+ 'io.modelcontextprotocol/clientCapabilities': capabilities,
+})
+
+let id = 0
+async function request({ method, params = {}, name, capabilities, headerMethod, envelope = true }) {
+ const body = JSON.stringify({
+ jsonrpc: '2.0',
+ id: ++id,
+ method,
+ params: envelope ? { ...params, _meta: { ...meta(capabilities), ...(params._meta ?? {}) } } : params,
+ })
+ let headers = {
+ 'content-type': 'application/json',
+ accept: 'application/json, text/event-stream',
+ ...(envelope && {
+ 'mcp-protocol-version': '2026-07-28',
+ 'mcp-method': headerMethod ?? method,
+ ...(name && { 'mcp-name': name }),
+ }),
+ }
+ if (signer) headers = await signer('POST', headers, body)
+ const t0 = Date.now()
+ const res = await fetch(url, { method: 'POST', headers, body })
+ const contentType = res.headers.get('content-type') ?? ''
+ if (contentType.includes('text/event-stream')) {
+ const events = []
+ const decoder = new TextDecoder()
+ let buffer = ''
+ for await (const chunk of res.body) {
+ buffer += decoder.decode(chunk, { stream: true })
+ let sep
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
+ const frame = buffer.slice(0, sep)
+ buffer = buffer.slice(sep + 2)
+ const data = frame.split('\n').find((l) => l.startsWith('data:'))?.slice(5).trim()
+ if (data) events.push({ t: Date.now() - t0, data: JSON.parse(data) })
+ }
+ }
+ const final = events.at(-1)?.data
+ return { status: res.status, contentType, events, json: final, ms: Date.now() - t0 }
+ }
+ const text = await res.text()
+ let json
+ try {
+ json = JSON.parse(text)
+ } catch {
+ json = { raw: text }
+ }
+ return { status: res.status, contentType, events: [], json, ms: Date.now() - t0 }
+}
+
+// --- tiny harness ------------------------------------------------------------
+
+const results = []
+async function check(label, fn) {
+ try {
+ const detail = await fn()
+ results.push({ label, ok: true })
+ console.log(`PASS ${label}${detail ? ` — ${detail}` : ''}`)
+ } catch (err) {
+ results.push({ label, ok: false })
+ console.log(`FAIL ${label} — ${err.message}`)
+ }
+}
+const assert = (cond, msg) => {
+ if (!cond) throw new Error(msg)
+}
+// AgentCore Runtime replaces 4xx response bodies with its own error envelope
+// (-32010, "Received error (400) ..."); the 400 status still proves the SDK
+// rejected the request, so the negative cases accept that form there.
+const rejectedWith = (json, code) =>
+ json.error?.code === code ||
+ (process.env.SERVICE === 'bedrock-agentcore' &&
+ json.error?.code === -32010 &&
+ /\(4\d\d\)/.test(json.error?.message ?? ''))
+
+// --- test cases ----------------------------------------------------------------
+
+await check('1. tools/list returns the canonical tools with cache hints', async () => {
+ const r = await request({ method: 'tools/list' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ const names = (r.json.result?.tools ?? []).map((t) => t.name).sort()
+ assert(JSON.stringify(names) === JSON.stringify(['add', 'approve_refund', 'slow_report']), `tools: ${names}`)
+ assert(r.json.result.ttlMs === 300000, `ttlMs ${r.json.result.ttlMs}`)
+ assert(r.json.result.cacheScope === 'public', `cacheScope ${r.json.result.cacheScope}`)
+ return `ttlMs=${r.json.result.ttlMs} cacheScope=${r.json.result.cacheScope}`
+})
+
+await check('2. add returns plain JSON with structured content', async () => {
+ const r = await request({ method: 'tools/call', params: { name: 'add', arguments: { a: 2, b: 40 } }, name: 'add' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ assert(r.json.result?.structuredContent?.sum === 42, JSON.stringify(r.json))
+ return 'sum=42'
+})
+
+await check('3. slow_report streams incremental progress over SSE', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 3 }, _meta: { progressToken: 'pt' } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('text/event-stream'), `content-type ${r.contentType}`)
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 3, `${progress.length} progress events`)
+ assert(progress[0].t < r.ms - 500, 'first progress event did not arrive before the final result')
+ assert(r.json.result?.content?.[0]?.text === 'completed 3 steps', JSON.stringify(r.json))
+ return `3 progress events, first at +${progress[0].t}ms, done in ${r.ms}ms`
+})
+
+await check('4. approve_refund elicitation round-trip (accept and cancel)', async () => {
+ const ask = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-1' } },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(ask.json.result?.resultType === 'input_required', JSON.stringify(ask.json))
+ assert(ask.json.result?.inputRequests?.confirm, 'missing inputRequests.confirm')
+ const accepted = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: true } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(accepted.json.result?.content?.[0]?.text === 'refunded o-1', JSON.stringify(accepted.json))
+ const cancelled = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: false } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(cancelled.json.result?.content?.[0]?.text === 'refund cancelled', JSON.stringify(cancelled.json))
+ // A real decline ACTION, not accept-with-false: a server missing the
+ // inputResponse action check re-asks on decline, and a real client loops.
+ const declined = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'decline' } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(declined.json.result?.content?.[0]?.text === 'refund cancelled', `decline must terminate, not re-ask: ${JSON.stringify(declined.json)}`)
+ return 'input_required -> refunded o-1 / refund cancelled / decline terminates'
+})
+
+await check('5. resources list + read (Mcp-Name carries the uri)', async () => {
+ const list = await request({ method: 'resources/list' })
+ const uris = (list.json.result?.resources ?? []).map((r) => r.uri)
+ assert(uris.includes('guide://usage'), `resources: ${uris}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'guide://usage' }, name: 'guide://usage' })
+ assert(read.json.result?.contents?.[0]?.text?.startsWith('# Usage'), JSON.stringify(read.json))
+ return 'guide://usage readable'
+})
+
+await check('6. resource template read with per-resource cache hint', async () => {
+ const templates = await request({ method: 'resources/templates/list' })
+ const uriTemplates = (templates.json.result?.resourceTemplates ?? []).map((t) => t.uriTemplate)
+ assert(uriTemplates.includes('orders://{orderId}'), `templates: ${uriTemplates}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'orders://o-42' }, name: 'orders://o-42' })
+ const record = JSON.parse(read.json.result?.contents?.[0]?.text ?? '{}')
+ assert(record.orderId === 'o-42' && record.status === 'shipped', JSON.stringify(read.json))
+ assert(read.json.result.ttlMs === 60000, `ttlMs ${read.json.result.ttlMs}`)
+ return `orders://o-42 -> ${record.status}, ttlMs=${read.json.result.ttlMs}`
+})
+
+await check('7. prompts list + get', async () => {
+ const list = await request({ method: 'prompts/list' })
+ const names = (list.json.result?.prompts ?? []).map((p) => p.name)
+ assert(names.includes('summarize_order'), `prompts: ${names}`)
+ const got = await request({
+ method: 'prompts/get',
+ params: { name: 'summarize_order', arguments: { orderId: 'o-7' } },
+ name: 'summarize_order',
+ })
+ const text = got.json.result?.messages?.[0]?.content?.text ?? ''
+ assert(text.includes('o-7'), JSON.stringify(got.json))
+ return 'summarize_order fills its argument'
+})
+
+await check('8. server/discover surfaces the instructions', async () => {
+ const r = await request({ method: 'server/discover' })
+ assert((r.json.result?.instructions ?? '').includes('Demo MCP server'), JSON.stringify(r.json.result ?? r.json))
+ return 'instructions present'
+})
+
+await check('9. legacy initialize is answered on the same endpoint', async () => {
+ const r = await request({
+ method: 'initialize',
+ params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'legacy', version: '1.0.0' } },
+ envelope: false,
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ const version = r.json.result?.protocolVersion ?? ''
+ assert(version.startsWith('2025-'), JSON.stringify(r.json))
+ return `served as ${version}`
+})
+
+await check('10. GET is answered with the spec-mandated 405', async () => {
+ let headers = { accept: 'application/json, text/event-stream' }
+ if (signer) headers = await signer('GET', headers, null)
+ const res = await fetch(url, { method: 'GET', headers })
+ assert(res.status === 405, `HTTP ${res.status}`)
+ return 'HTTP 405'
+})
+
+await check('11. Mcp-Method header mismatching the body is rejected (-32020)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'add', arguments: { a: 1, b: 1 } },
+ name: 'add',
+ headerMethod: 'tools/list',
+ })
+ assert(rejectedWith(r.json, -32020), JSON.stringify(r.json))
+ return r.json.error.code === -32020 ? '-32020' : 'rejected (platform-wrapped 400)'
+})
+
+await check('12. elicitation without the client capability is rejected (-32021)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-9' } },
+ name: 'approve_refund',
+ capabilities: {},
+ })
+ assert(rejectedWith(r.json, -32021), JSON.stringify(r.json))
+ return r.json.error.code === -32021 ? '-32021' : 'rejected (platform-wrapped 400)'
+})
+
+if (process.env.LONG) {
+ await check('13. LONG: 45-step stream (~36s) survives past the 29s mark', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 }, _meta: { progressToken: 'long' } },
+ name: 'slow_report',
+ })
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 45, `${progress.length} progress events`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', 'final result missing (stream cut?)')
+ assert(r.ms > 34000, `finished suspiciously fast (${r.ms}ms)`)
+ return `45 events over ${(r.ms / 1000).toFixed(1)}s, final result received`
+ })
+}
+
+if (process.env.LONG) {
+ await check('14. LONG: a silent 36s call is not cut short', async () => {
+ // slow_report emits progress only when given a progressToken, so this call
+ // writes nothing at all for ~36s and then returns plain JSON. That is the
+ // case an edge-optimized API Gateway endpoint kills at 30s (its idle
+ // timeout starts at invoke, not at the first byte, and a raised
+ // timeoutInMillis does not help) - which is why the REST-fronted examples
+ // deploy on a regional endpoint.
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status} after ${(r.ms / 1000).toFixed(1)}s`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', JSON.stringify(r.json).slice(0, 200))
+ return `plain JSON after ${(r.ms / 1000).toFixed(1)}s of silence`
+ })
+}
+
+// --- summary -------------------------------------------------------------------
+
+const failed = results.filter((r) => !r.ok)
+console.log(`\n${results.length - failed.length}/${results.length} passed`)
+process.exit(failed.length ? 1 : 0)
diff --git a/mcp/custom/express-web-adapter/README.md b/mcp/custom/express-web-adapter/README.md
new file mode 100644
index 000000000..28bc21b18
--- /dev/null
+++ b/mcp/custom/express-web-adapter/README.md
@@ -0,0 +1,86 @@
+
+
+# MCP Server with Express + Lambda Web Adapter
+
+Run a [Model Context Protocol](https://modelcontextprotocol.io) server as a **plain Express app** on AWS Lambda. [AWS Lambda Web Adapter](https://github.com/aws/aws-lambda-web-adapter) (a public layer configured entirely in `serverless.yml`) proxies Lambda events to the HTTP server over localhost - the application code contains nothing Lambda-specific and runs unchanged on Fargate, EC2, or your laptop. The Express integration is the MCP SDK's official [`@modelcontextprotocol/express`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/middleware/express) package.
+
+> **The web framework and the packaging are independent choices.** This example pairs Express with a **zip** deployment because zip needs no Docker; the [fastify-container](../fastify-container) sibling pairs Fastify with a **container image**. Swap either dimension - Express in a container, Fastify in a zip - without touching application code.
+
+This is one of the [custom MCP hosting](../README.md) examples - they all serve the **same canonical MCP server** (`src/server.mjs`) and differ only in the hosting glue, so the same test client works against every one of them.
+
+How the pieces fit:
+
+- **`src/index.mjs`** - `createMcpExpressApp()` + one route mounting the MCP handler via `toNodeHandler`, listening on `PORT`.
+- **`run.sh`** - the Lambda "handler": the adapter layer's exec wrapper runs it to boot the HTTP server, then forwards invocations to it.
+- **`serverless.yml`** - the Lambda Web Adapter layer + `AWS_LWA_INVOKE_MODE: response_stream`, behind API Gateway REST with `response.transferMode: stream` (streaming works end to end, SSE included).
+
+## Usage
+
+### Deploy
+
+```bash
+npm install
+serverless deploy
+```
+
+### Test
+
+Every example in this family ships the same test client:
+
+```bash
+ENDPOINT= node client.mjs
+```
+
+```
+PASS 1. tools/list returns the canonical tools with cache hints — ttlMs=300000 cacheScope=public
+PASS 2. add returns plain JSON with structured content — sum=42
+PASS 3. slow_report streams incremental progress over SSE — 3 progress events, first at +810ms
+PASS 4. approve_refund elicitation round-trip (accept and cancel) — input_required -> refunded o-1 / refund cancelled
+PASS 5. resources list + read (Mcp-Name carries the uri) — guide://usage readable
+PASS 6. resource template read with per-resource cache hint — orders://o-42 -> shipped, ttlMs=60000
+PASS 7. prompts list + get — summarize_order fills its argument
+PASS 8. server/discover surfaces the instructions — instructions present
+PASS 9. legacy initialize is answered on the same endpoint — served as 2025-06-18
+PASS 10. GET is answered with the spec-mandated 405 — HTTP 405
+PASS 11. Mcp-Method header mismatching the body is rejected (-32020) — -32020
+PASS 12. elicitation without the client capability is rejected (-32021) — -32021
+
+12/12 passed
+```
+
+Add `LONG=1` for a ~36-second streaming case proving streams run far past API Gateway's classic 29-second ceiling.
+
+You can also run the server locally - it is just an Express app:
+
+```bash
+PORT=8000 node src/index.mjs
+ENDPOINT=http://localhost:8000/mcp node client.mjs
+```
+
+### Authentication
+
+The demo endpoint is public. For real deployments (both included):
+
+- **Reject at the gateway** - uncomment the `authorizer` wiring in `serverless.yml`: [src/authorizer.mjs](src/authorizer.mjs) is a complete Lambda authorizer validating Bearer JWTs against any OpenID Connect provider.
+- **Validate in-process** - the SDK's `requireBearerAuth` with a `jose` JWKS verifier (commented block at the bottom of `src/server.mjs`).
+- Behind an IAM-auth Function URL, the adapter's `AWS_LWA_AUTHORIZATION_SOURCE` can restore a client-supplied token from another header into `Authorization` (SigV4 reserves the original).
+
+### Going further
+
+- **Long-running tools**: raise the function `timeout` and `timeoutInMillis` together. Keep the endpoint regional (as configured): edge-optimized REST endpoints cut streams that stay idle for 30 seconds, so a tool that computes quietly for longer than that would fail there even with a raised timeout.
+
+### Cleanup
+
+```bash
+serverless remove
+```
diff --git a/mcp/custom/express-web-adapter/client.mjs b/mcp/custom/express-web-adapter/client.mjs
new file mode 100644
index 000000000..7ea0a3700
--- /dev/null
+++ b/mcp/custom/express-web-adapter/client.mjs
@@ -0,0 +1,348 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the mcp/custom examples. Every example serves
+ * the same canonical MCP server, so this same script tests all of them.
+ *
+ * Usage:
+ * ENDPOINT= node client.mjs
+ * LONG=1 ENDPOINT=... node client.mjs # adds a ~36s streaming case
+ * AUTH=sigv4 SERVICE=bedrock-agentcore ENDPOINT= node client.mjs
+ * AUTH=sigv4 SERVICE=lambda ENDPOINT=... node client.mjs # IAM-auth Function URL
+ *
+ * Exercises the full 2026-07-28 surface: plain JSON, SSE progress streaming,
+ * an elicitation round-trip, resources (static + template), prompts,
+ * server/discover, cache hints, the legacy fallback, and two negative cases
+ * (header mismatch -32020, missing capability -32021).
+ */
+
+const ENDPOINT = process.env.ENDPOINT
+if (!ENDPOINT) {
+ console.error('Set ENDPOINT to the MCP endpoint URL')
+ process.exit(1)
+}
+
+// --- endpoint + optional SigV4 signing --------------------------------------
+
+let url = new URL(ENDPOINT)
+let signer
+if (process.env.AUTH === 'sigv4') {
+ const service = process.env.SERVICE || 'lambda'
+ if (service === 'bedrock-agentcore' && url.pathname.includes('/runtimes/')) {
+ // AgentCore runtime URLs embed the agent ARN - the WHOLE ARN must be
+ // URL-encoded in the path (including its inner "/").
+ const arn = decodeURIComponent(url.pathname.match(/\/runtimes\/(.+?)\/invocations/)[1])
+ url = new URL(`${url.origin}/runtimes/${encodeURIComponent(arn)}/invocations`)
+ url.searchParams.set('qualifier', 'DEFAULT')
+ }
+ const [{ SignatureV4 }, { HttpRequest }, { Sha256 }, { defaultProvider }] = await Promise.all([
+ import('@smithy/signature-v4'),
+ import('@smithy/protocol-http'),
+ import('@aws-crypto/sha256-js'),
+ import('@aws-sdk/credential-provider-node'),
+ ])
+ const sigv4 = new SignatureV4({
+ service,
+ region: process.env.AWS_REGION || 'us-east-1',
+ credentials: defaultProvider(),
+ sha256: Sha256,
+ })
+ signer = async (method, headers, body) => {
+ const request = new HttpRequest({
+ method,
+ protocol: url.protocol,
+ hostname: url.hostname,
+ path: url.pathname,
+ query: Object.fromEntries(url.searchParams),
+ headers: { host: url.hostname, ...headers },
+ ...(body != null && { body }),
+ })
+ return (await sigv4.sign(request)).headers
+ }
+}
+
+// --- request helpers ---------------------------------------------------------
+
+const CAPS_ELICIT = { elicitation: { form: {} } }
+const meta = (capabilities = {}) => ({
+ 'io.modelcontextprotocol/protocolVersion': '2026-07-28',
+ 'io.modelcontextprotocol/clientInfo': { name: 'aws-mcp-servers-client', version: '1.0.0' },
+ 'io.modelcontextprotocol/clientCapabilities': capabilities,
+})
+
+let id = 0
+async function request({ method, params = {}, name, capabilities, headerMethod, envelope = true }) {
+ const body = JSON.stringify({
+ jsonrpc: '2.0',
+ id: ++id,
+ method,
+ params: envelope ? { ...params, _meta: { ...meta(capabilities), ...(params._meta ?? {}) } } : params,
+ })
+ let headers = {
+ 'content-type': 'application/json',
+ accept: 'application/json, text/event-stream',
+ ...(envelope && {
+ 'mcp-protocol-version': '2026-07-28',
+ 'mcp-method': headerMethod ?? method,
+ ...(name && { 'mcp-name': name }),
+ }),
+ }
+ if (signer) headers = await signer('POST', headers, body)
+ const t0 = Date.now()
+ const res = await fetch(url, { method: 'POST', headers, body })
+ const contentType = res.headers.get('content-type') ?? ''
+ if (contentType.includes('text/event-stream')) {
+ const events = []
+ const decoder = new TextDecoder()
+ let buffer = ''
+ for await (const chunk of res.body) {
+ buffer += decoder.decode(chunk, { stream: true })
+ let sep
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
+ const frame = buffer.slice(0, sep)
+ buffer = buffer.slice(sep + 2)
+ const data = frame.split('\n').find((l) => l.startsWith('data:'))?.slice(5).trim()
+ if (data) events.push({ t: Date.now() - t0, data: JSON.parse(data) })
+ }
+ }
+ const final = events.at(-1)?.data
+ return { status: res.status, contentType, events, json: final, ms: Date.now() - t0 }
+ }
+ const text = await res.text()
+ let json
+ try {
+ json = JSON.parse(text)
+ } catch {
+ json = { raw: text }
+ }
+ return { status: res.status, contentType, events: [], json, ms: Date.now() - t0 }
+}
+
+// --- tiny harness ------------------------------------------------------------
+
+const results = []
+async function check(label, fn) {
+ try {
+ const detail = await fn()
+ results.push({ label, ok: true })
+ console.log(`PASS ${label}${detail ? ` — ${detail}` : ''}`)
+ } catch (err) {
+ results.push({ label, ok: false })
+ console.log(`FAIL ${label} — ${err.message}`)
+ }
+}
+const assert = (cond, msg) => {
+ if (!cond) throw new Error(msg)
+}
+// AgentCore Runtime replaces 4xx response bodies with its own error envelope
+// (-32010, "Received error (400) ..."); the 400 status still proves the SDK
+// rejected the request, so the negative cases accept that form there.
+const rejectedWith = (json, code) =>
+ json.error?.code === code ||
+ (process.env.SERVICE === 'bedrock-agentcore' &&
+ json.error?.code === -32010 &&
+ /\(4\d\d\)/.test(json.error?.message ?? ''))
+
+// --- test cases ----------------------------------------------------------------
+
+await check('1. tools/list returns the canonical tools with cache hints', async () => {
+ const r = await request({ method: 'tools/list' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ const names = (r.json.result?.tools ?? []).map((t) => t.name).sort()
+ assert(JSON.stringify(names) === JSON.stringify(['add', 'approve_refund', 'slow_report']), `tools: ${names}`)
+ assert(r.json.result.ttlMs === 300000, `ttlMs ${r.json.result.ttlMs}`)
+ assert(r.json.result.cacheScope === 'public', `cacheScope ${r.json.result.cacheScope}`)
+ return `ttlMs=${r.json.result.ttlMs} cacheScope=${r.json.result.cacheScope}`
+})
+
+await check('2. add returns plain JSON with structured content', async () => {
+ const r = await request({ method: 'tools/call', params: { name: 'add', arguments: { a: 2, b: 40 } }, name: 'add' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ assert(r.json.result?.structuredContent?.sum === 42, JSON.stringify(r.json))
+ return 'sum=42'
+})
+
+await check('3. slow_report streams incremental progress over SSE', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 3 }, _meta: { progressToken: 'pt' } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('text/event-stream'), `content-type ${r.contentType}`)
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 3, `${progress.length} progress events`)
+ assert(progress[0].t < r.ms - 500, 'first progress event did not arrive before the final result')
+ assert(r.json.result?.content?.[0]?.text === 'completed 3 steps', JSON.stringify(r.json))
+ return `3 progress events, first at +${progress[0].t}ms, done in ${r.ms}ms`
+})
+
+await check('4. approve_refund elicitation round-trip (accept and cancel)', async () => {
+ const ask = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-1' } },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(ask.json.result?.resultType === 'input_required', JSON.stringify(ask.json))
+ assert(ask.json.result?.inputRequests?.confirm, 'missing inputRequests.confirm')
+ const accepted = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: true } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(accepted.json.result?.content?.[0]?.text === 'refunded o-1', JSON.stringify(accepted.json))
+ const cancelled = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: false } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(cancelled.json.result?.content?.[0]?.text === 'refund cancelled', JSON.stringify(cancelled.json))
+ // A real decline ACTION, not accept-with-false: a server missing the
+ // inputResponse action check re-asks on decline, and a real client loops.
+ const declined = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'decline' } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(declined.json.result?.content?.[0]?.text === 'refund cancelled', `decline must terminate, not re-ask: ${JSON.stringify(declined.json)}`)
+ return 'input_required -> refunded o-1 / refund cancelled / decline terminates'
+})
+
+await check('5. resources list + read (Mcp-Name carries the uri)', async () => {
+ const list = await request({ method: 'resources/list' })
+ const uris = (list.json.result?.resources ?? []).map((r) => r.uri)
+ assert(uris.includes('guide://usage'), `resources: ${uris}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'guide://usage' }, name: 'guide://usage' })
+ assert(read.json.result?.contents?.[0]?.text?.startsWith('# Usage'), JSON.stringify(read.json))
+ return 'guide://usage readable'
+})
+
+await check('6. resource template read with per-resource cache hint', async () => {
+ const templates = await request({ method: 'resources/templates/list' })
+ const uriTemplates = (templates.json.result?.resourceTemplates ?? []).map((t) => t.uriTemplate)
+ assert(uriTemplates.includes('orders://{orderId}'), `templates: ${uriTemplates}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'orders://o-42' }, name: 'orders://o-42' })
+ const record = JSON.parse(read.json.result?.contents?.[0]?.text ?? '{}')
+ assert(record.orderId === 'o-42' && record.status === 'shipped', JSON.stringify(read.json))
+ assert(read.json.result.ttlMs === 60000, `ttlMs ${read.json.result.ttlMs}`)
+ return `orders://o-42 -> ${record.status}, ttlMs=${read.json.result.ttlMs}`
+})
+
+await check('7. prompts list + get', async () => {
+ const list = await request({ method: 'prompts/list' })
+ const names = (list.json.result?.prompts ?? []).map((p) => p.name)
+ assert(names.includes('summarize_order'), `prompts: ${names}`)
+ const got = await request({
+ method: 'prompts/get',
+ params: { name: 'summarize_order', arguments: { orderId: 'o-7' } },
+ name: 'summarize_order',
+ })
+ const text = got.json.result?.messages?.[0]?.content?.text ?? ''
+ assert(text.includes('o-7'), JSON.stringify(got.json))
+ return 'summarize_order fills its argument'
+})
+
+await check('8. server/discover surfaces the instructions', async () => {
+ const r = await request({ method: 'server/discover' })
+ assert((r.json.result?.instructions ?? '').includes('Demo MCP server'), JSON.stringify(r.json.result ?? r.json))
+ return 'instructions present'
+})
+
+await check('9. legacy initialize is answered on the same endpoint', async () => {
+ const r = await request({
+ method: 'initialize',
+ params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'legacy', version: '1.0.0' } },
+ envelope: false,
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ const version = r.json.result?.protocolVersion ?? ''
+ assert(version.startsWith('2025-'), JSON.stringify(r.json))
+ return `served as ${version}`
+})
+
+await check('10. GET is answered with the spec-mandated 405', async () => {
+ let headers = { accept: 'application/json, text/event-stream' }
+ if (signer) headers = await signer('GET', headers, null)
+ const res = await fetch(url, { method: 'GET', headers })
+ assert(res.status === 405, `HTTP ${res.status}`)
+ return 'HTTP 405'
+})
+
+await check('11. Mcp-Method header mismatching the body is rejected (-32020)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'add', arguments: { a: 1, b: 1 } },
+ name: 'add',
+ headerMethod: 'tools/list',
+ })
+ assert(rejectedWith(r.json, -32020), JSON.stringify(r.json))
+ return r.json.error.code === -32020 ? '-32020' : 'rejected (platform-wrapped 400)'
+})
+
+await check('12. elicitation without the client capability is rejected (-32021)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-9' } },
+ name: 'approve_refund',
+ capabilities: {},
+ })
+ assert(rejectedWith(r.json, -32021), JSON.stringify(r.json))
+ return r.json.error.code === -32021 ? '-32021' : 'rejected (platform-wrapped 400)'
+})
+
+if (process.env.LONG) {
+ await check('13. LONG: 45-step stream (~36s) survives past the 29s mark', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 }, _meta: { progressToken: 'long' } },
+ name: 'slow_report',
+ })
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 45, `${progress.length} progress events`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', 'final result missing (stream cut?)')
+ assert(r.ms > 34000, `finished suspiciously fast (${r.ms}ms)`)
+ return `45 events over ${(r.ms / 1000).toFixed(1)}s, final result received`
+ })
+}
+
+if (process.env.LONG) {
+ await check('14. LONG: a silent 36s call is not cut short', async () => {
+ // slow_report emits progress only when given a progressToken, so this call
+ // writes nothing at all for ~36s and then returns plain JSON. That is the
+ // case an edge-optimized API Gateway endpoint kills at 30s (its idle
+ // timeout starts at invoke, not at the first byte, and a raised
+ // timeoutInMillis does not help) - which is why the REST-fronted examples
+ // deploy on a regional endpoint.
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status} after ${(r.ms / 1000).toFixed(1)}s`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', JSON.stringify(r.json).slice(0, 200))
+ return `plain JSON after ${(r.ms / 1000).toFixed(1)}s of silence`
+ })
+}
+
+// --- summary -------------------------------------------------------------------
+
+const failed = results.filter((r) => !r.ok)
+console.log(`\n${results.length - failed.length}/${results.length} passed`)
+process.exit(failed.length ? 1 : 0)
diff --git a/mcp/custom/express-web-adapter/package-lock.json b/mcp/custom/express-web-adapter/package-lock.json
new file mode 100644
index 000000000..4c07ded24
--- /dev/null
+++ b/mcp/custom/express-web-adapter/package-lock.json
@@ -0,0 +1,998 @@
+{
+ "name": "aws-mcp-express",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "aws-mcp-express",
+ "version": "1.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/express": "^2.0.0",
+ "@modelcontextprotocol/node": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
+ "express": "^5.1.0",
+ "jose": "^6.0.0",
+ "zod": "^4.2.0"
+ }
+ },
+ "node_modules/@hono/node-server": {
+ "version": "1.19.17",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz",
+ "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
+ "node_modules/@modelcontextprotocol/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/express": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/express/-/express-2.0.0.tgz",
+ "integrity": "sha512-Snlr8j9FR9LcVvEJPF7qJ7d5zTL4Bes2dk7RcacN9eSZ7OLohwxqhEvWu1+UxELyScgLbLLOdeVEGdwKI1iwVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cors": "^2.8.5"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "express": "^4.18.0 || ^5.0.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/node": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz",
+ "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "hono": "^4.11.4"
+ },
+ "peerDependenciesMeta": {
+ "hono": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@modelcontextprotocol/server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/core": "2.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
+ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^2.0.0",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.1",
+ "iconv-lite": "^0.7.2",
+ "on-finished": "^2.4.1",
+ "qs": "^6.15.2",
+ "raw-body": "^3.0.2",
+ "type-is": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/body-parser/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hono": {
+ "version": "4.12.32",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz",
+ "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/jose": {
+ "version": "6.2.4",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz",
+ "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
+ "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
+ "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
+ "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/mcp/custom/express-web-adapter/package.json b/mcp/custom/express-web-adapter/package.json
new file mode 100644
index 000000000..5216406f1
--- /dev/null
+++ b/mcp/custom/express-web-adapter/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "aws-mcp-express",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "MCP server as a plain Express app on AWS Lambda via Lambda Web Adapter (zip deployment)",
+ "dependencies": {
+ "@modelcontextprotocol/express": "^2.0.0",
+ "@modelcontextprotocol/node": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
+ "express": "^5.1.0",
+ "jose": "^6.0.0",
+ "zod": "^4.2.0"
+ }
+}
diff --git a/mcp/custom/express-web-adapter/run.sh b/mcp/custom/express-web-adapter/run.sh
new file mode 100755
index 000000000..ca91880bc
--- /dev/null
+++ b/mcp/custom/express-web-adapter/run.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+exec node src/index.mjs
diff --git a/mcp/custom/express-web-adapter/serverless.yml b/mcp/custom/express-web-adapter/serverless.yml
new file mode 100644
index 000000000..c2c455041
--- /dev/null
+++ b/mcp/custom/express-web-adapter/serverless.yml
@@ -0,0 +1,60 @@
+# MCP server as a plain Express app on Lambda, via AWS Lambda Web Adapter.
+#
+# The Lambda Web Adapter layer proxies Lambda events to the Express server
+# over localhost HTTP - the application code contains nothing Lambda-specific.
+# Served through API Gateway REST response streaming.
+service: aws-mcp-express
+
+provider:
+ name: aws
+ runtime: nodejs22.x # the MCP SDK v2 requires Node.js 20+
+ # Regional endpoint, not the default edge-optimized one: edge REST endpoints
+ # cut idle streams after 30 seconds, which kills a tool that computes quietly
+ # for longer than that before responding. Regional raises that bound to
+ # 5 minutes (verified: an identical 36s silent call returns 200 on regional
+ # and 504 on edge).
+ endpointType: REGIONAL
+
+functions:
+ mcp:
+ # The handler is the startup script (the layer's exec wrapper runs it and
+ # proxies invocations to the HTTP server it starts):
+ handler: run.sh
+ timeout: 120
+ layers:
+ # AWS Lambda Web Adapter, published by AWS (region-agnostic account,
+ # arch-specific): https://github.com/aws/aws-lambda-web-adapter
+ - arn:aws:lambda:${aws:region}:753240598075:layer:LambdaAdapterLayerX86:25
+ environment:
+ AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap
+ AWS_LWA_INVOKE_MODE: response_stream # stream responses (SSE) end-to-end
+ PORT: '8000'
+ events:
+ - http:
+ method: POST
+ path: /mcp
+ # Response streaming lifts API Gateway's classic response limits and
+ # enables SSE, but the integration timeout must be raised explicitly
+ # alongside it (up to 15 minutes):
+ timeoutInMillis: 120000
+ response:
+ transferMode: stream
+ # To reject unauthenticated requests before they invoke the function,
+ # attach the JWT authorizer below (src/authorizer.mjs validates
+ # Bearer tokens against any OpenID Connect provider), or use the
+ # SDK's in-process bearer auth (see src/server.mjs):
+ # authorizer:
+ # name: jwtAuthorizer
+ - http:
+ method: GET
+ path: /mcp # the server answers the spec-mandated 405 for GET
+ response:
+ transferMode: stream # every route on a streaming handler streams
+
+ # The request authorizer pairing with the `authorizer` block above:
+ # jwtAuthorizer:
+ # handler: src/authorizer.handler
+ # environment:
+ # JWKS_URL: https:///.well-known/jwks.json
+ # TOKEN_ISSUER: https:///
+ # TOKEN_AUDIENCE: https://
diff --git a/mcp/custom/express-web-adapter/src/authorizer.mjs b/mcp/custom/express-web-adapter/src/authorizer.mjs
new file mode 100644
index 000000000..6e451fced
--- /dev/null
+++ b/mcp/custom/express-web-adapter/src/authorizer.mjs
@@ -0,0 +1,33 @@
+/**
+ * A Lambda TOKEN authorizer validating OAuth Bearer JWTs against any OpenID
+ * Connect provider - API Gateway rejects unauthenticated requests before they
+ * ever invoke (and bill) the MCP function.
+ *
+ * Configure via environment variables on the authorizer function:
+ * JWKS_URL e.g. https://your-tenant.example.com/.well-known/jwks.json
+ * TOKEN_ISSUER e.g. https://your-tenant.example.com/
+ * TOKEN_AUDIENCE e.g. https://your-mcp-endpoint
+ */
+import { createRemoteJWKSet, jwtVerify } from 'jose'
+
+const jwks = createRemoteJWKSet(new URL(process.env.JWKS_URL))
+
+export const handler = async (event) => {
+ const token = event.authorizationToken?.replace(/^Bearer /i, '')
+ try {
+ const { payload } = await jwtVerify(token, jwks, {
+ issuer: process.env.TOKEN_ISSUER,
+ audience: process.env.TOKEN_AUDIENCE,
+ })
+ return {
+ principalId: payload.sub,
+ policyDocument: {
+ Version: '2012-10-17',
+ Statement: [{ Action: 'execute-api:Invoke', Effect: 'Allow', Resource: event.methodArn }],
+ },
+ }
+ } catch {
+ // API Gateway maps this exact message to a 401 response.
+ throw new Error('Unauthorized')
+ }
+}
diff --git a/mcp/custom/express-web-adapter/src/index.mjs b/mcp/custom/express-web-adapter/src/index.mjs
new file mode 100644
index 000000000..8187e52df
--- /dev/null
+++ b/mcp/custom/express-web-adapter/src/index.mjs
@@ -0,0 +1,29 @@
+/**
+ * A real Express app serving the MCP handler - no Lambda-specific code at
+ * all. AWS Lambda Web Adapter (configured in serverless.yml) proxies Lambda
+ * events to this HTTP server over localhost; the same file runs unchanged on
+ * Fargate, EC2, or your laptop.
+ *
+ * The web framework and the packaging are independent choices: this example
+ * pairs Express with a zip deployment because zip needs no Docker; the
+ * fastify-container sibling pairs Fastify with a container image. Swap either
+ * dimension without touching application code.
+ */
+import { createMcpExpressApp } from '@modelcontextprotocol/express'
+import { toNodeHandler } from '@modelcontextprotocol/node'
+import mcp from './server.mjs'
+
+// host: '0.0.0.0' declares this app as intentionally exposed, which disables
+// the factory's localhost DNS-rebinding protections - correct behind a cloud
+// front door, where the platform already pins the Host header.
+const app = createMcpExpressApp({ host: '0.0.0.0' })
+
+// createMcpExpressApp installs express.json(), so the parsed body rides
+// along. Forward it only for POST: express.json() defaults req.body to {}
+// on bodyless requests, and a present-but-empty parsed body changes how the
+// handler classifies them.
+const node = toNodeHandler(mcp)
+app.all('/mcp', (req, res) => node(req, res, req.method === 'POST' ? req.body : undefined))
+
+const port = Number(process.env.PORT ?? 8000)
+app.listen(port, () => console.log(`MCP server listening on http://0.0.0.0:${port}/mcp`))
diff --git a/mcp/custom/express-web-adapter/src/server.mjs b/mcp/custom/express-web-adapter/src/server.mjs
new file mode 100644
index 000000000..0a8c36a1b
--- /dev/null
+++ b/mcp/custom/express-web-adapter/src/server.mjs
@@ -0,0 +1,204 @@
+/**
+ * A standard MCP server built with the official MCP TypeScript SDK v2,
+ * serving the stateless MCP 2026-07-28 protocol revision (and answering
+ * older clients through the SDK's built-in fallback on the same endpoint).
+ *
+ * Nothing in this file is Lambda-specific - the same handler runs on any
+ * web-standard host. Every example in mcp/custom/ serves this exact
+ * server; only the hosting glue around it differs.
+ *
+ * Surface:
+ * tools: add (plain JSON), slow_report (progress streaming),
+ * approve_refund (elicitation - pauses to ask the user)
+ * resources: guide://usage, orders://{orderId} (template)
+ * prompts: summarize_order
+ * extras: instructions (served via server/discover), cache hints
+ */
+import {
+ acceptedContent,
+ createMcpHandler,
+ inputRequired,
+ inputResponse,
+ McpServer,
+ ResourceTemplate,
+} from '@modelcontextprotocol/server'
+import { z } from 'zod'
+
+const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
+
+export default createMcpHandler(() => {
+ const server = new McpServer(
+ { name: 'aws-mcp-server', version: '1.0.0' },
+ {
+ // Shown to clients that probe the server/discover method:
+ instructions:
+ 'Demo MCP server. Call add for arithmetic, slow_report to watch ' +
+ 'streamed progress, approve_refund to see a tool ask the user for ' +
+ 'confirmation mid-call. Read guide://usage for a walkthrough.',
+ // Cache hints: let clients and shared caches reuse the tool list for
+ // five minutes. Without hints the SDK emits the conservative defaults
+ // (ttlMs: 0, cacheScope: private) on every cacheable result:
+ cacheHints: { 'tools/list': { ttlMs: 300000, cacheScope: 'public' } },
+ },
+ )
+
+ // A plain tool: zod schemas next to the implementation; returning
+ // structuredContent lets clients consume typed results.
+ server.registerTool(
+ 'add',
+ {
+ description: 'Add two numbers',
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
+ outputSchema: z.object({ sum: z.number() }),
+ },
+ async ({ a, b }) => {
+ const output = { sum: a + b }
+ return {
+ content: [{ type: 'text', text: String(output.sum) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ // A long-running tool emitting progress notifications. When the client
+ // requests progress (a progressToken in _meta) and accepts text/event-stream,
+ // the response is served as SSE with notifications ahead of the final result.
+ server.registerTool(
+ 'slow_report',
+ {
+ description: 'Generate a report, reporting progress along the way',
+ inputSchema: z.object({ steps: z.number().default(3) }),
+ },
+ async ({ steps }, ctx) => {
+ // Real clients request progress via `_meta.progressToken`, never as a
+ // tool argument.
+ const progressToken = ctx.mcpReq._meta?.progressToken
+ for (let i = 1; i <= steps; i++) {
+ await sleep(800)
+ if (progressToken) {
+ await ctx.mcpReq.notify({
+ method: 'notifications/progress',
+ params: { progressToken, progress: i, total: steps, message: `step ${i}` },
+ })
+ }
+ }
+ return { content: [{ type: 'text', text: `completed ${steps} steps` }] }
+ },
+ )
+
+ // Elicitation - a tool that pauses to ask the user for input. The handler
+ // returns an input_required result; the client asks the user and retries
+ // the call with the answers attached, re-entering the handler, which reads
+ // them with acceptedContent. Works statelessly - no session required.
+ server.registerTool(
+ 'approve_refund',
+ {
+ description: 'Refund an order after user confirmation',
+ inputSchema: z.object({ orderId: z.string() }),
+ },
+ async ({ orderId }, ctx) => {
+ const answer = acceptedContent(
+ ctx.mcpReq.inputResponses,
+ 'confirm',
+ z.object({ confirmed: z.boolean() }),
+ )
+ // Decline/cancel must terminate: acceptedContent returns undefined for
+ // BOTH "not asked yet" and "user said no" - without this check a decline
+ // falls through to inputRequired and the client re-prompts forever.
+ const confirmView = inputResponse(ctx.mcpReq.inputResponses, 'confirm')
+ if (confirmView.kind === 'elicit' && confirmView.action !== 'accept') {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ if (answer === undefined) {
+ return inputRequired({
+ inputRequests: {
+ confirm: inputRequired.elicit({
+ message: `Refund order ${orderId}?`,
+ requestedSchema: z.object({ confirmed: z.boolean() }),
+ }),
+ },
+ })
+ }
+ if (!answer.confirmed) {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ return { content: [{ type: 'text', text: `refunded ${orderId}` }] }
+ },
+ )
+
+ // A readable document next to the tools. Clients fetch it with
+ // resources/read (the Mcp-Name header carries the uri).
+ server.registerResource(
+ 'usage-guide',
+ 'guide://usage',
+ { description: 'How to use this server', mimeType: 'text/markdown' },
+ async (uri) => ({
+ contents: [
+ {
+ uri: uri.href,
+ text: '# Usage\n\nCall `add` for sums, `slow_report` for streamed progress, `approve_refund` for an elicitation round-trip.',
+ },
+ ],
+ }),
+ )
+
+ // A resource template - parameterized URIs resolved per request. The
+ // per-resource cacheHint overrides the server-level hints for its reads.
+ server.registerResource(
+ 'order',
+ new ResourceTemplate('orders://{orderId}', { list: undefined }),
+ { description: 'Order record by id', cacheHint: { ttlMs: 60000, cacheScope: 'private' } },
+ async (uri, { orderId }) => ({
+ contents: [{ uri: uri.href, text: JSON.stringify({ orderId, status: 'shipped' }) }],
+ }),
+ )
+
+ // A prompt template clients can list and fill in.
+ server.registerPrompt(
+ 'summarize_order',
+ {
+ description: 'Ask the model to summarize an order',
+ argsSchema: z.object({ orderId: z.string() }),
+ },
+ ({ orderId }) => ({
+ messages: [
+ {
+ role: 'user',
+ content: { type: 'text', text: `Summarize the status of order ${orderId} in one sentence.` },
+ },
+ ],
+ }),
+ )
+
+ return server
+})
+
+// ---------------------------------------------------------------------------
+// Going further (uncomment and adapt):
+// ---------------------------------------------------------------------------
+//
+// Signed round-trip state - when an elicitation round-trip must carry server
+// state the retry cannot be allowed to tamper with (a computed price, an
+// idempotency key), return it as requestState sealed with the SDK's HMAC
+// codec. Store the >= 32-byte key in Secrets Manager or SSM:
+//
+// import { createRequestStateCodec } from '@modelcontextprotocol/server'
+// const codec = createRequestStateCodec({ key: process.env.MCP_STATE_KEY, ttlSeconds: 600 })
+//
+// In-process OAuth (works identically behind every front door): the SDK ships
+// bearer-token middleware and OAuth discovery-document helpers. Wrap the
+// handler's fetch face before exporting - any OpenID Connect provider works:
+//
+// import { requireBearerAuth, oauthMetadataResponse } from '@modelcontextprotocol/server'
+// import { createRemoteJWKSet, jwtVerify } from 'jose'
+//
+// const jwks = createRemoteJWKSet(new URL('https:///.well-known/jwks.json'))
+// const verifier = {
+// async verifyAccessToken(token) {
+// const { payload } = await jwtVerify(token, jwks, {
+// issuer: 'https:///',
+// audience: 'https://',
+// })
+// return { token, clientId: payload.azp, scopes: payload.scope?.split(' ') ?? [], expiresAt: payload.exp }
+// },
+// }
diff --git a/mcp/custom/fastify-container/Dockerfile b/mcp/custom/fastify-container/Dockerfile
new file mode 100644
index 000000000..b9d26fa5d
--- /dev/null
+++ b/mcp/custom/fastify-container/Dockerfile
@@ -0,0 +1,24 @@
+# A portable HTTP-server image made Lambda-compatible by the Lambda Web
+# Adapter extension - the only Lambda-specific line is the COPY below. The
+# same image runs unchanged on Fargate, App Runner, or any container host.
+#
+# The platform is pinned so the image matches the Lambda function's
+# architecture (x86_64 by default) no matter what machine builds it - an
+# image built on an Apple Silicon laptop without the pin fails at invoke
+# with "exec format error". For Graviton, switch this to linux/arm64 and
+# set `architecture: arm64` on the function.
+FROM --platform=linux/amd64 public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 AS lambda-adapter
+
+FROM --platform=linux/amd64 public.ecr.aws/docker/library/node:22-slim
+
+COPY --from=lambda-adapter /lambda-adapter /opt/extensions/lambda-adapter
+
+ENV PORT=8000 AWS_LWA_INVOKE_MODE=response_stream
+
+WORKDIR /app
+COPY package.json package-lock.json ./
+RUN npm ci --omit=dev
+COPY src ./src
+
+EXPOSE 8000
+CMD ["node", "src/index.mjs"]
diff --git a/mcp/custom/fastify-container/README.md b/mcp/custom/fastify-container/README.md
new file mode 100644
index 000000000..5567cb567
--- /dev/null
+++ b/mcp/custom/fastify-container/README.md
@@ -0,0 +1,80 @@
+
+
+# MCP Server with Fastify in a container image
+
+Run a [Model Context Protocol](https://modelcontextprotocol.io) server as a **containerized Fastify app** on AWS Lambda. The subject of this example is the packaging: a portable container image whose only Lambda-specific line is the [AWS Lambda Web Adapter](https://github.com/aws/aws-lambda-web-adapter) extension `COPY` in the Dockerfile - the same image runs unchanged on Fargate, App Runner, or any container host. The Fastify integration is the MCP SDK's official [`@modelcontextprotocol/fastify`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/middleware/fastify) package.
+
+> **The web framework and the packaging are independent choices.** This example pairs Fastify with a **container image**; the [express-web-adapter](../express-web-adapter) sibling pairs Express with a **zip** deployment (no Docker needed). Swap either dimension - Fastify in a zip, Express in a container - without touching application code.
+
+This is one of the [custom MCP hosting](../README.md) examples - they all serve the **same canonical MCP server** (`src/server.mjs`) and differ only in the hosting glue, so the same test client works against every one of them.
+
+How the pieces fit:
+
+- **`src/index.mjs`** - `createMcpFastifyApp()` + one route mounting the MCP handler via `toNodeHandler` (with `reply.hijack()` so SSE streams flow on the raw response), listening on `PORT`.
+- **`Dockerfile`** - `node:22-slim` + the Lambda Web Adapter extension + the app. `AWS_LWA_INVOKE_MODE=response_stream` streams responses end to end.
+- **`serverless.yml`** - an ECR image build (`provider.ecr.images`) wired to the function, served on a Function URL with `invokeMode: RESPONSE_STREAM`.
+
+## Usage
+
+### Deploy
+
+Docker must be running - deploying builds the image and pushes it to ECR:
+
+```bash
+npm install
+serverless deploy
+```
+
+### Test
+
+Every example in this family ships the same test client:
+
+```bash
+ENDPOINT=mcp node client.mjs
+```
+
+```
+PASS 1. tools/list returns the canonical tools with cache hints — ttlMs=300000 cacheScope=public
+PASS 2. add returns plain JSON with structured content — sum=42
+PASS 3. slow_report streams incremental progress over SSE — 3 progress events, first at +810ms
+PASS 4. approve_refund elicitation round-trip (accept and cancel) — input_required -> refunded o-1 / refund cancelled
+PASS 5. resources list + read (Mcp-Name carries the uri) — guide://usage readable
+PASS 6. resource template read with per-resource cache hint — orders://o-42 -> shipped, ttlMs=60000
+PASS 7. prompts list + get — summarize_order fills its argument
+PASS 8. server/discover surfaces the instructions — instructions present
+PASS 9. legacy initialize is answered on the same endpoint — served as 2025-06-18
+PASS 10. GET is answered with the spec-mandated 405 — HTTP 405
+PASS 11. Mcp-Method header mismatching the body is rejected (-32020) — -32020
+PASS 12. elicitation without the client capability is rejected (-32021) — -32021
+
+12/12 passed
+```
+
+Add `LONG=1` for a ~36-second streaming case - on Function URLs the function `timeout` is the only bound.
+
+You can also run the server locally - it is just a Fastify app (or `docker build` and run the image):
+
+```bash
+PORT=8000 node src/index.mjs
+ENDPOINT=http://localhost:8000/mcp node client.mjs
+```
+
+### Authentication
+
+The demo endpoint is public. For real deployments (both included as comments): uncomment `authorizer: aws_iam` in `serverless.yml` for AWS-credentialed callers (the shared client tests this mode with `AUTH=sigv4 SERVICE=lambda`), or validate OAuth Bearer tokens in-process with the SDK's `requireBearerAuth` and a `jose` JWKS verifier (commented block at the bottom of `src/server.mjs`).
+
+### Cleanup
+
+```bash
+serverless remove
+```
diff --git a/mcp/custom/fastify-container/client.mjs b/mcp/custom/fastify-container/client.mjs
new file mode 100644
index 000000000..7ea0a3700
--- /dev/null
+++ b/mcp/custom/fastify-container/client.mjs
@@ -0,0 +1,348 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the mcp/custom examples. Every example serves
+ * the same canonical MCP server, so this same script tests all of them.
+ *
+ * Usage:
+ * ENDPOINT= node client.mjs
+ * LONG=1 ENDPOINT=... node client.mjs # adds a ~36s streaming case
+ * AUTH=sigv4 SERVICE=bedrock-agentcore ENDPOINT= node client.mjs
+ * AUTH=sigv4 SERVICE=lambda ENDPOINT=... node client.mjs # IAM-auth Function URL
+ *
+ * Exercises the full 2026-07-28 surface: plain JSON, SSE progress streaming,
+ * an elicitation round-trip, resources (static + template), prompts,
+ * server/discover, cache hints, the legacy fallback, and two negative cases
+ * (header mismatch -32020, missing capability -32021).
+ */
+
+const ENDPOINT = process.env.ENDPOINT
+if (!ENDPOINT) {
+ console.error('Set ENDPOINT to the MCP endpoint URL')
+ process.exit(1)
+}
+
+// --- endpoint + optional SigV4 signing --------------------------------------
+
+let url = new URL(ENDPOINT)
+let signer
+if (process.env.AUTH === 'sigv4') {
+ const service = process.env.SERVICE || 'lambda'
+ if (service === 'bedrock-agentcore' && url.pathname.includes('/runtimes/')) {
+ // AgentCore runtime URLs embed the agent ARN - the WHOLE ARN must be
+ // URL-encoded in the path (including its inner "/").
+ const arn = decodeURIComponent(url.pathname.match(/\/runtimes\/(.+?)\/invocations/)[1])
+ url = new URL(`${url.origin}/runtimes/${encodeURIComponent(arn)}/invocations`)
+ url.searchParams.set('qualifier', 'DEFAULT')
+ }
+ const [{ SignatureV4 }, { HttpRequest }, { Sha256 }, { defaultProvider }] = await Promise.all([
+ import('@smithy/signature-v4'),
+ import('@smithy/protocol-http'),
+ import('@aws-crypto/sha256-js'),
+ import('@aws-sdk/credential-provider-node'),
+ ])
+ const sigv4 = new SignatureV4({
+ service,
+ region: process.env.AWS_REGION || 'us-east-1',
+ credentials: defaultProvider(),
+ sha256: Sha256,
+ })
+ signer = async (method, headers, body) => {
+ const request = new HttpRequest({
+ method,
+ protocol: url.protocol,
+ hostname: url.hostname,
+ path: url.pathname,
+ query: Object.fromEntries(url.searchParams),
+ headers: { host: url.hostname, ...headers },
+ ...(body != null && { body }),
+ })
+ return (await sigv4.sign(request)).headers
+ }
+}
+
+// --- request helpers ---------------------------------------------------------
+
+const CAPS_ELICIT = { elicitation: { form: {} } }
+const meta = (capabilities = {}) => ({
+ 'io.modelcontextprotocol/protocolVersion': '2026-07-28',
+ 'io.modelcontextprotocol/clientInfo': { name: 'aws-mcp-servers-client', version: '1.0.0' },
+ 'io.modelcontextprotocol/clientCapabilities': capabilities,
+})
+
+let id = 0
+async function request({ method, params = {}, name, capabilities, headerMethod, envelope = true }) {
+ const body = JSON.stringify({
+ jsonrpc: '2.0',
+ id: ++id,
+ method,
+ params: envelope ? { ...params, _meta: { ...meta(capabilities), ...(params._meta ?? {}) } } : params,
+ })
+ let headers = {
+ 'content-type': 'application/json',
+ accept: 'application/json, text/event-stream',
+ ...(envelope && {
+ 'mcp-protocol-version': '2026-07-28',
+ 'mcp-method': headerMethod ?? method,
+ ...(name && { 'mcp-name': name }),
+ }),
+ }
+ if (signer) headers = await signer('POST', headers, body)
+ const t0 = Date.now()
+ const res = await fetch(url, { method: 'POST', headers, body })
+ const contentType = res.headers.get('content-type') ?? ''
+ if (contentType.includes('text/event-stream')) {
+ const events = []
+ const decoder = new TextDecoder()
+ let buffer = ''
+ for await (const chunk of res.body) {
+ buffer += decoder.decode(chunk, { stream: true })
+ let sep
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
+ const frame = buffer.slice(0, sep)
+ buffer = buffer.slice(sep + 2)
+ const data = frame.split('\n').find((l) => l.startsWith('data:'))?.slice(5).trim()
+ if (data) events.push({ t: Date.now() - t0, data: JSON.parse(data) })
+ }
+ }
+ const final = events.at(-1)?.data
+ return { status: res.status, contentType, events, json: final, ms: Date.now() - t0 }
+ }
+ const text = await res.text()
+ let json
+ try {
+ json = JSON.parse(text)
+ } catch {
+ json = { raw: text }
+ }
+ return { status: res.status, contentType, events: [], json, ms: Date.now() - t0 }
+}
+
+// --- tiny harness ------------------------------------------------------------
+
+const results = []
+async function check(label, fn) {
+ try {
+ const detail = await fn()
+ results.push({ label, ok: true })
+ console.log(`PASS ${label}${detail ? ` — ${detail}` : ''}`)
+ } catch (err) {
+ results.push({ label, ok: false })
+ console.log(`FAIL ${label} — ${err.message}`)
+ }
+}
+const assert = (cond, msg) => {
+ if (!cond) throw new Error(msg)
+}
+// AgentCore Runtime replaces 4xx response bodies with its own error envelope
+// (-32010, "Received error (400) ..."); the 400 status still proves the SDK
+// rejected the request, so the negative cases accept that form there.
+const rejectedWith = (json, code) =>
+ json.error?.code === code ||
+ (process.env.SERVICE === 'bedrock-agentcore' &&
+ json.error?.code === -32010 &&
+ /\(4\d\d\)/.test(json.error?.message ?? ''))
+
+// --- test cases ----------------------------------------------------------------
+
+await check('1. tools/list returns the canonical tools with cache hints', async () => {
+ const r = await request({ method: 'tools/list' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ const names = (r.json.result?.tools ?? []).map((t) => t.name).sort()
+ assert(JSON.stringify(names) === JSON.stringify(['add', 'approve_refund', 'slow_report']), `tools: ${names}`)
+ assert(r.json.result.ttlMs === 300000, `ttlMs ${r.json.result.ttlMs}`)
+ assert(r.json.result.cacheScope === 'public', `cacheScope ${r.json.result.cacheScope}`)
+ return `ttlMs=${r.json.result.ttlMs} cacheScope=${r.json.result.cacheScope}`
+})
+
+await check('2. add returns plain JSON with structured content', async () => {
+ const r = await request({ method: 'tools/call', params: { name: 'add', arguments: { a: 2, b: 40 } }, name: 'add' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ assert(r.json.result?.structuredContent?.sum === 42, JSON.stringify(r.json))
+ return 'sum=42'
+})
+
+await check('3. slow_report streams incremental progress over SSE', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 3 }, _meta: { progressToken: 'pt' } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('text/event-stream'), `content-type ${r.contentType}`)
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 3, `${progress.length} progress events`)
+ assert(progress[0].t < r.ms - 500, 'first progress event did not arrive before the final result')
+ assert(r.json.result?.content?.[0]?.text === 'completed 3 steps', JSON.stringify(r.json))
+ return `3 progress events, first at +${progress[0].t}ms, done in ${r.ms}ms`
+})
+
+await check('4. approve_refund elicitation round-trip (accept and cancel)', async () => {
+ const ask = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-1' } },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(ask.json.result?.resultType === 'input_required', JSON.stringify(ask.json))
+ assert(ask.json.result?.inputRequests?.confirm, 'missing inputRequests.confirm')
+ const accepted = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: true } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(accepted.json.result?.content?.[0]?.text === 'refunded o-1', JSON.stringify(accepted.json))
+ const cancelled = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: false } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(cancelled.json.result?.content?.[0]?.text === 'refund cancelled', JSON.stringify(cancelled.json))
+ // A real decline ACTION, not accept-with-false: a server missing the
+ // inputResponse action check re-asks on decline, and a real client loops.
+ const declined = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'decline' } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(declined.json.result?.content?.[0]?.text === 'refund cancelled', `decline must terminate, not re-ask: ${JSON.stringify(declined.json)}`)
+ return 'input_required -> refunded o-1 / refund cancelled / decline terminates'
+})
+
+await check('5. resources list + read (Mcp-Name carries the uri)', async () => {
+ const list = await request({ method: 'resources/list' })
+ const uris = (list.json.result?.resources ?? []).map((r) => r.uri)
+ assert(uris.includes('guide://usage'), `resources: ${uris}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'guide://usage' }, name: 'guide://usage' })
+ assert(read.json.result?.contents?.[0]?.text?.startsWith('# Usage'), JSON.stringify(read.json))
+ return 'guide://usage readable'
+})
+
+await check('6. resource template read with per-resource cache hint', async () => {
+ const templates = await request({ method: 'resources/templates/list' })
+ const uriTemplates = (templates.json.result?.resourceTemplates ?? []).map((t) => t.uriTemplate)
+ assert(uriTemplates.includes('orders://{orderId}'), `templates: ${uriTemplates}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'orders://o-42' }, name: 'orders://o-42' })
+ const record = JSON.parse(read.json.result?.contents?.[0]?.text ?? '{}')
+ assert(record.orderId === 'o-42' && record.status === 'shipped', JSON.stringify(read.json))
+ assert(read.json.result.ttlMs === 60000, `ttlMs ${read.json.result.ttlMs}`)
+ return `orders://o-42 -> ${record.status}, ttlMs=${read.json.result.ttlMs}`
+})
+
+await check('7. prompts list + get', async () => {
+ const list = await request({ method: 'prompts/list' })
+ const names = (list.json.result?.prompts ?? []).map((p) => p.name)
+ assert(names.includes('summarize_order'), `prompts: ${names}`)
+ const got = await request({
+ method: 'prompts/get',
+ params: { name: 'summarize_order', arguments: { orderId: 'o-7' } },
+ name: 'summarize_order',
+ })
+ const text = got.json.result?.messages?.[0]?.content?.text ?? ''
+ assert(text.includes('o-7'), JSON.stringify(got.json))
+ return 'summarize_order fills its argument'
+})
+
+await check('8. server/discover surfaces the instructions', async () => {
+ const r = await request({ method: 'server/discover' })
+ assert((r.json.result?.instructions ?? '').includes('Demo MCP server'), JSON.stringify(r.json.result ?? r.json))
+ return 'instructions present'
+})
+
+await check('9. legacy initialize is answered on the same endpoint', async () => {
+ const r = await request({
+ method: 'initialize',
+ params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'legacy', version: '1.0.0' } },
+ envelope: false,
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ const version = r.json.result?.protocolVersion ?? ''
+ assert(version.startsWith('2025-'), JSON.stringify(r.json))
+ return `served as ${version}`
+})
+
+await check('10. GET is answered with the spec-mandated 405', async () => {
+ let headers = { accept: 'application/json, text/event-stream' }
+ if (signer) headers = await signer('GET', headers, null)
+ const res = await fetch(url, { method: 'GET', headers })
+ assert(res.status === 405, `HTTP ${res.status}`)
+ return 'HTTP 405'
+})
+
+await check('11. Mcp-Method header mismatching the body is rejected (-32020)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'add', arguments: { a: 1, b: 1 } },
+ name: 'add',
+ headerMethod: 'tools/list',
+ })
+ assert(rejectedWith(r.json, -32020), JSON.stringify(r.json))
+ return r.json.error.code === -32020 ? '-32020' : 'rejected (platform-wrapped 400)'
+})
+
+await check('12. elicitation without the client capability is rejected (-32021)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-9' } },
+ name: 'approve_refund',
+ capabilities: {},
+ })
+ assert(rejectedWith(r.json, -32021), JSON.stringify(r.json))
+ return r.json.error.code === -32021 ? '-32021' : 'rejected (platform-wrapped 400)'
+})
+
+if (process.env.LONG) {
+ await check('13. LONG: 45-step stream (~36s) survives past the 29s mark', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 }, _meta: { progressToken: 'long' } },
+ name: 'slow_report',
+ })
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 45, `${progress.length} progress events`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', 'final result missing (stream cut?)')
+ assert(r.ms > 34000, `finished suspiciously fast (${r.ms}ms)`)
+ return `45 events over ${(r.ms / 1000).toFixed(1)}s, final result received`
+ })
+}
+
+if (process.env.LONG) {
+ await check('14. LONG: a silent 36s call is not cut short', async () => {
+ // slow_report emits progress only when given a progressToken, so this call
+ // writes nothing at all for ~36s and then returns plain JSON. That is the
+ // case an edge-optimized API Gateway endpoint kills at 30s (its idle
+ // timeout starts at invoke, not at the first byte, and a raised
+ // timeoutInMillis does not help) - which is why the REST-fronted examples
+ // deploy on a regional endpoint.
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status} after ${(r.ms / 1000).toFixed(1)}s`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', JSON.stringify(r.json).slice(0, 200))
+ return `plain JSON after ${(r.ms / 1000).toFixed(1)}s of silence`
+ })
+}
+
+// --- summary -------------------------------------------------------------------
+
+const failed = results.filter((r) => !r.ok)
+console.log(`\n${results.length - failed.length}/${results.length} passed`)
+process.exit(failed.length ? 1 : 0)
diff --git a/mcp/custom/fastify-container/package-lock.json b/mcp/custom/fastify-container/package-lock.json
new file mode 100644
index 000000000..240a7e706
--- /dev/null
+++ b/mcp/custom/fastify-container/package-lock.json
@@ -0,0 +1,741 @@
+{
+ "name": "aws-mcp-fastify-container",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "aws-mcp-fastify-container",
+ "version": "1.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/fastify": "^2.0.0",
+ "@modelcontextprotocol/node": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
+ "fastify": "^5.6.0",
+ "zod": "^4.2.0"
+ }
+ },
+ "node_modules/@fastify/ajv-compiler": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz",
+ "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.12.0",
+ "ajv-formats": "^3.0.1",
+ "fast-uri": "^3.0.0"
+ }
+ },
+ "node_modules/@fastify/error": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
+ "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@fastify/fast-json-stringify-compiler": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz",
+ "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "fast-json-stringify": "^7.0.0"
+ }
+ },
+ "node_modules/@fastify/forwarded": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.2.tgz",
+ "integrity": "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@fastify/merge-json-schemas": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz",
+ "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/@fastify/proxy-addr": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz",
+ "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/forwarded": "^3.0.0",
+ "ipaddr.js": "^2.1.0"
+ }
+ },
+ "node_modules/@hono/node-server": {
+ "version": "1.19.17",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz",
+ "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
+ "node_modules/@modelcontextprotocol/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/fastify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/fastify/-/fastify-2.0.0.tgz",
+ "integrity": "sha512-x41AmyQ+olgAjX8EBkVpCztxIcWaInUlVpEatUBSWINZAmTnQYSYqOp9eigb7dxE2W+CtzhP1H+9uUgHRxJPVg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "fastify": "^5.2.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/node": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz",
+ "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "hono": "^4.11.4"
+ },
+ "peerDependenciesMeta": {
+ "hono": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@modelcontextprotocol/server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/core": "2.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@pinojs/redact": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
+ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
+ "license": "MIT"
+ },
+ "node_modules/abstract-logging": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
+ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==",
+ "license": "MIT"
+ },
+ "node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/atomic-sleep": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
+ "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/avvio": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz",
+ "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/error": "^4.0.0",
+ "fastq": "^1.17.1"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fast-decode-uri-component": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
+ "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stringify": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz",
+ "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/merge-json-schemas": "^0.2.0",
+ "ajv": "^8.12.0",
+ "ajv-formats": "^3.0.1",
+ "fast-uri": "^4.0.0",
+ "json-schema-ref-resolver": "^3.0.0",
+ "rfdc": "^1.2.0"
+ }
+ },
+ "node_modules/fast-json-stringify/node_modules/fast-uri": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.1.tgz",
+ "integrity": "sha512-YPOs1zD5TG2+EZt+r88LwF6mclA7TPkpwMP7ZN3TO2HiHS8TXvq7QA/17iJsV9dubcLo/f8eEYqMBruyQV21hQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fast-querystring": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
+ "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-decode-uri-component": "^1.0.1"
+ }
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
+ "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fastify": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.10.0.tgz",
+ "integrity": "sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/ajv-compiler": "^4.0.5",
+ "@fastify/error": "^4.0.0",
+ "@fastify/fast-json-stringify-compiler": "^5.0.0",
+ "@fastify/proxy-addr": "^5.0.0",
+ "abstract-logging": "^2.0.1",
+ "avvio": "^9.0.0",
+ "fast-json-stringify": "^7.0.0",
+ "find-my-way": "^9.6.0",
+ "light-my-request": "^6.0.0",
+ "pino": "^9.14.0 || ^10.1.0",
+ "process-warning": "^5.0.0",
+ "rfdc": "^1.3.1",
+ "secure-json-parse": "^4.0.0",
+ "semver": "^7.6.0",
+ "toad-cache": "^3.7.0"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/find-my-way": {
+ "version": "9.7.0",
+ "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz",
+ "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-querystring": "^1.0.0",
+ "safe-regex2": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/hono": {
+ "version": "4.12.32",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz",
+ "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz",
+ "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/json-schema-ref-resolver": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz",
+ "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/light-my-request": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz",
+ "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "process-warning": "^4.0.0",
+ "set-cookie-parser": "^2.6.0"
+ }
+ },
+ "node_modules/light-my-request/node_modules/process-warning": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
+ "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/on-exit-leak-free": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
+ "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/pino": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
+ "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
+ "license": "MIT",
+ "dependencies": {
+ "@pinojs/redact": "^0.4.0",
+ "atomic-sleep": "^1.0.0",
+ "on-exit-leak-free": "^2.1.0",
+ "pino-abstract-transport": "^3.0.0",
+ "pino-std-serializers": "^7.0.0",
+ "process-warning": "^5.0.0",
+ "quick-format-unescaped": "^4.0.3",
+ "real-require": "^0.2.0",
+ "safe-stable-stringify": "^2.3.1",
+ "sonic-boom": "^4.0.1",
+ "thread-stream": "^4.0.0"
+ },
+ "bin": {
+ "pino": "bin.js"
+ }
+ },
+ "node_modules/pino-abstract-transport": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
+ "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.0.0"
+ }
+ },
+ "node_modules/pino-std-serializers": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
+ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
+ "license": "MIT"
+ },
+ "node_modules/process-warning": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
+ "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/quick-format-unescaped": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
+ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
+ "license": "MIT"
+ },
+ "node_modules/real-require": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
+ "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ret": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz",
+ "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "license": "MIT"
+ },
+ "node_modules/safe-regex2": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz",
+ "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "ret": "~0.5.0"
+ },
+ "bin": {
+ "safe-regex2": "bin/safe-regex2.js"
+ }
+ },
+ "node_modules/safe-stable-stringify": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+ "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/secure-json-parse": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
+ "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/sonic-boom": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
+ "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "atomic-sleep": "^1.0.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/thread-stream": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
+ "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "real-require": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/thread-stream/node_modules/real-require": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz",
+ "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
+ "license": "MIT"
+ },
+ "node_modules/toad-cache": {
+ "version": "3.7.4",
+ "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz",
+ "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/mcp/custom/fastify-container/package.json b/mcp/custom/fastify-container/package.json
new file mode 100644
index 000000000..2ad00f00a
--- /dev/null
+++ b/mcp/custom/fastify-container/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "aws-mcp-fastify-container",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "MCP server as a containerized Fastify app on AWS Lambda via Lambda Web Adapter (container image deployment)",
+ "dependencies": {
+ "@modelcontextprotocol/fastify": "^2.0.0",
+ "@modelcontextprotocol/node": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
+ "fastify": "^5.6.0",
+ "zod": "^4.2.0"
+ }
+}
diff --git a/mcp/custom/fastify-container/serverless.yml b/mcp/custom/fastify-container/serverless.yml
new file mode 100644
index 000000000..dc2ae3c5a
--- /dev/null
+++ b/mcp/custom/fastify-container/serverless.yml
@@ -0,0 +1,26 @@
+# MCP server as a containerized Fastify app on Lambda, via AWS Lambda Web
+# Adapter baked into the image (see Dockerfile). Deploying builds the image
+# and pushes it to ECR (Docker must be running). Served on a Function URL
+# with response streaming.
+service: aws-mcp-fastify-container
+
+provider:
+ name: aws
+ ecr:
+ images:
+ mcp:
+ path: ./
+
+functions:
+ mcp:
+ image:
+ name: mcp
+ timeout: 120 # streamed responses can run up to the full function timeout
+ url:
+ invokeMode: RESPONSE_STREAM
+ # The default (no authorizer) is a public endpoint - fine for trying the
+ # example, but add auth for real deployments:
+ # - IAM (SigV4) for AWS-credentialed callers:
+ # authorizer: aws_iam
+ # - or OAuth validated in-process with the SDK's requireBearerAuth - see
+ # the commented block in src/server.mjs.
diff --git a/mcp/custom/fastify-container/src/index.mjs b/mcp/custom/fastify-container/src/index.mjs
new file mode 100644
index 000000000..d67c8e64b
--- /dev/null
+++ b/mcp/custom/fastify-container/src/index.mjs
@@ -0,0 +1,30 @@
+/**
+ * A real Fastify app serving the MCP handler - no Lambda-specific code at
+ * all. AWS Lambda Web Adapter (baked into the container image as an
+ * extension) proxies Lambda events to this HTTP server over localhost; the
+ * same image runs unchanged on Fargate, App Runner, or anywhere else.
+ *
+ * The web framework and the packaging are independent choices: this example
+ * pairs Fastify with a container image; the express-web-adapter sibling pairs
+ * Express with a zip deployment. Swap either dimension without touching
+ * application code.
+ */
+import { createMcpFastifyApp } from '@modelcontextprotocol/fastify'
+import { toNodeHandler } from '@modelcontextprotocol/node'
+import mcp from './server.mjs'
+
+// host: '0.0.0.0' declares this app as intentionally exposed, which disables
+// the factory's localhost DNS-rebinding protections - correct behind a cloud
+// front door, where the platform already pins the Host header.
+const app = createMcpFastifyApp({ host: '0.0.0.0' })
+
+const node = toNodeHandler(mcp)
+app.all('/mcp', (request, reply) => {
+ // Hand the raw response over to the MCP adapter (SSE needs the real stream).
+ reply.hijack()
+ node(request.raw, reply.raw, request.body)
+})
+
+const port = Number(process.env.PORT ?? 8000)
+await app.listen({ port, host: '0.0.0.0' })
+console.log(`MCP server listening on http://0.0.0.0:${port}/mcp`)
diff --git a/mcp/custom/fastify-container/src/server.mjs b/mcp/custom/fastify-container/src/server.mjs
new file mode 100644
index 000000000..0a8c36a1b
--- /dev/null
+++ b/mcp/custom/fastify-container/src/server.mjs
@@ -0,0 +1,204 @@
+/**
+ * A standard MCP server built with the official MCP TypeScript SDK v2,
+ * serving the stateless MCP 2026-07-28 protocol revision (and answering
+ * older clients through the SDK's built-in fallback on the same endpoint).
+ *
+ * Nothing in this file is Lambda-specific - the same handler runs on any
+ * web-standard host. Every example in mcp/custom/ serves this exact
+ * server; only the hosting glue around it differs.
+ *
+ * Surface:
+ * tools: add (plain JSON), slow_report (progress streaming),
+ * approve_refund (elicitation - pauses to ask the user)
+ * resources: guide://usage, orders://{orderId} (template)
+ * prompts: summarize_order
+ * extras: instructions (served via server/discover), cache hints
+ */
+import {
+ acceptedContent,
+ createMcpHandler,
+ inputRequired,
+ inputResponse,
+ McpServer,
+ ResourceTemplate,
+} from '@modelcontextprotocol/server'
+import { z } from 'zod'
+
+const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
+
+export default createMcpHandler(() => {
+ const server = new McpServer(
+ { name: 'aws-mcp-server', version: '1.0.0' },
+ {
+ // Shown to clients that probe the server/discover method:
+ instructions:
+ 'Demo MCP server. Call add for arithmetic, slow_report to watch ' +
+ 'streamed progress, approve_refund to see a tool ask the user for ' +
+ 'confirmation mid-call. Read guide://usage for a walkthrough.',
+ // Cache hints: let clients and shared caches reuse the tool list for
+ // five minutes. Without hints the SDK emits the conservative defaults
+ // (ttlMs: 0, cacheScope: private) on every cacheable result:
+ cacheHints: { 'tools/list': { ttlMs: 300000, cacheScope: 'public' } },
+ },
+ )
+
+ // A plain tool: zod schemas next to the implementation; returning
+ // structuredContent lets clients consume typed results.
+ server.registerTool(
+ 'add',
+ {
+ description: 'Add two numbers',
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
+ outputSchema: z.object({ sum: z.number() }),
+ },
+ async ({ a, b }) => {
+ const output = { sum: a + b }
+ return {
+ content: [{ type: 'text', text: String(output.sum) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ // A long-running tool emitting progress notifications. When the client
+ // requests progress (a progressToken in _meta) and accepts text/event-stream,
+ // the response is served as SSE with notifications ahead of the final result.
+ server.registerTool(
+ 'slow_report',
+ {
+ description: 'Generate a report, reporting progress along the way',
+ inputSchema: z.object({ steps: z.number().default(3) }),
+ },
+ async ({ steps }, ctx) => {
+ // Real clients request progress via `_meta.progressToken`, never as a
+ // tool argument.
+ const progressToken = ctx.mcpReq._meta?.progressToken
+ for (let i = 1; i <= steps; i++) {
+ await sleep(800)
+ if (progressToken) {
+ await ctx.mcpReq.notify({
+ method: 'notifications/progress',
+ params: { progressToken, progress: i, total: steps, message: `step ${i}` },
+ })
+ }
+ }
+ return { content: [{ type: 'text', text: `completed ${steps} steps` }] }
+ },
+ )
+
+ // Elicitation - a tool that pauses to ask the user for input. The handler
+ // returns an input_required result; the client asks the user and retries
+ // the call with the answers attached, re-entering the handler, which reads
+ // them with acceptedContent. Works statelessly - no session required.
+ server.registerTool(
+ 'approve_refund',
+ {
+ description: 'Refund an order after user confirmation',
+ inputSchema: z.object({ orderId: z.string() }),
+ },
+ async ({ orderId }, ctx) => {
+ const answer = acceptedContent(
+ ctx.mcpReq.inputResponses,
+ 'confirm',
+ z.object({ confirmed: z.boolean() }),
+ )
+ // Decline/cancel must terminate: acceptedContent returns undefined for
+ // BOTH "not asked yet" and "user said no" - without this check a decline
+ // falls through to inputRequired and the client re-prompts forever.
+ const confirmView = inputResponse(ctx.mcpReq.inputResponses, 'confirm')
+ if (confirmView.kind === 'elicit' && confirmView.action !== 'accept') {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ if (answer === undefined) {
+ return inputRequired({
+ inputRequests: {
+ confirm: inputRequired.elicit({
+ message: `Refund order ${orderId}?`,
+ requestedSchema: z.object({ confirmed: z.boolean() }),
+ }),
+ },
+ })
+ }
+ if (!answer.confirmed) {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ return { content: [{ type: 'text', text: `refunded ${orderId}` }] }
+ },
+ )
+
+ // A readable document next to the tools. Clients fetch it with
+ // resources/read (the Mcp-Name header carries the uri).
+ server.registerResource(
+ 'usage-guide',
+ 'guide://usage',
+ { description: 'How to use this server', mimeType: 'text/markdown' },
+ async (uri) => ({
+ contents: [
+ {
+ uri: uri.href,
+ text: '# Usage\n\nCall `add` for sums, `slow_report` for streamed progress, `approve_refund` for an elicitation round-trip.',
+ },
+ ],
+ }),
+ )
+
+ // A resource template - parameterized URIs resolved per request. The
+ // per-resource cacheHint overrides the server-level hints for its reads.
+ server.registerResource(
+ 'order',
+ new ResourceTemplate('orders://{orderId}', { list: undefined }),
+ { description: 'Order record by id', cacheHint: { ttlMs: 60000, cacheScope: 'private' } },
+ async (uri, { orderId }) => ({
+ contents: [{ uri: uri.href, text: JSON.stringify({ orderId, status: 'shipped' }) }],
+ }),
+ )
+
+ // A prompt template clients can list and fill in.
+ server.registerPrompt(
+ 'summarize_order',
+ {
+ description: 'Ask the model to summarize an order',
+ argsSchema: z.object({ orderId: z.string() }),
+ },
+ ({ orderId }) => ({
+ messages: [
+ {
+ role: 'user',
+ content: { type: 'text', text: `Summarize the status of order ${orderId} in one sentence.` },
+ },
+ ],
+ }),
+ )
+
+ return server
+})
+
+// ---------------------------------------------------------------------------
+// Going further (uncomment and adapt):
+// ---------------------------------------------------------------------------
+//
+// Signed round-trip state - when an elicitation round-trip must carry server
+// state the retry cannot be allowed to tamper with (a computed price, an
+// idempotency key), return it as requestState sealed with the SDK's HMAC
+// codec. Store the >= 32-byte key in Secrets Manager or SSM:
+//
+// import { createRequestStateCodec } from '@modelcontextprotocol/server'
+// const codec = createRequestStateCodec({ key: process.env.MCP_STATE_KEY, ttlSeconds: 600 })
+//
+// In-process OAuth (works identically behind every front door): the SDK ships
+// bearer-token middleware and OAuth discovery-document helpers. Wrap the
+// handler's fetch face before exporting - any OpenID Connect provider works:
+//
+// import { requireBearerAuth, oauthMetadataResponse } from '@modelcontextprotocol/server'
+// import { createRemoteJWKSet, jwtVerify } from 'jose'
+//
+// const jwks = createRemoteJWKSet(new URL('https:///.well-known/jwks.json'))
+// const verifier = {
+// async verifyAccessToken(token) {
+// const { payload } = await jwtVerify(token, jwks, {
+// issuer: 'https:///',
+// audience: 'https://',
+// })
+// return { token, clientId: payload.azp, scopes: payload.scope?.split(' ') ?? [], expiresAt: payload.exp }
+// },
+// }
diff --git a/mcp/custom/function-url/README.md b/mcp/custom/function-url/README.md
new file mode 100644
index 000000000..620535e4b
--- /dev/null
+++ b/mcp/custom/function-url/README.md
@@ -0,0 +1,77 @@
+
+
+# MCP Server on a Lambda Function URL
+
+Run a [Model Context Protocol](https://modelcontextprotocol.io) server built with the **official [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)** on a Lambda Function URL with response streaming - the leanest MCP hosting on AWS: no API Gateway, no per-request fee, and streamed responses bounded only by the function timeout.
+
+This is one of the [custom MCP hosting](../README.md) examples - they all serve the **same canonical MCP server** and differ only in the hosting glue, so the same test client works against every one of them. Three parts:
+
+- **`src/server.mjs`** - the canonical server: tools (`add`, `slow_report` with streamed progress, `approve_refund` with an elicitation round-trip), resources (`guide://usage` plus an `orders://{orderId}` template), a prompt, `instructions` served via `server/discover`, and cache hints on the tool list. Nothing Lambda-specific.
+- **`src/lambda.mjs`** - ~80 lines of adapter glue mapping the Function URL event and the Lambda response stream onto the Node-style shapes that `@modelcontextprotocol/node` accepts. No MCP logic. (For glue-free alternatives built on existing packages, see the [hono](../hono) and [express-web-adapter](../express-web-adapter) examples.)
+- **`serverless.yml`** - a Function URL with `invokeMode: RESPONSE_STREAM`.
+
+Plain JSON and streaming SSE coexist on the one endpoint. **Related:** [rest-api](../rest-api) serves the same server through API Gateway REST in stream mode - pick that one when you want a custom domain, WAF, throttling, or an authorizer that rejects requests before they invoke the function; pick this one for the simplest and cheapest setup.
+
+## Usage
+
+### Deploy
+
+```bash
+npm install
+serverless deploy
+```
+
+### Test
+
+Every example in this family ships the same test client:
+
+```bash
+ENDPOINT=mcp node client.mjs
+```
+
+```
+PASS 1. tools/list returns the canonical tools with cache hints — ttlMs=300000 cacheScope=public
+PASS 2. add returns plain JSON with structured content — sum=42
+PASS 3. slow_report streams incremental progress over SSE — 3 progress events, first at +810ms
+PASS 4. approve_refund elicitation round-trip (accept and cancel) — input_required -> refunded o-1 / refund cancelled
+PASS 5. resources list + read (Mcp-Name carries the uri) — guide://usage readable
+PASS 6. resource template read with per-resource cache hint — orders://o-42 -> shipped, ttlMs=60000
+PASS 7. prompts list + get — summarize_order fills its argument
+PASS 8. server/discover surfaces the instructions — instructions present
+PASS 9. legacy initialize is answered on the same endpoint — served as 2025-06-18
+PASS 10. GET is answered with the spec-mandated 405 — HTTP 405
+PASS 11. Mcp-Method header mismatching the body is rejected (-32020) — -32020
+PASS 12. elicitation without the client capability is rejected (-32021) — -32021
+
+12/12 passed
+```
+
+Add `LONG=1` for a ~36-second streaming case - on Function URLs no extra timeout configuration is needed; the function `timeout` is the only bound.
+
+### Authentication
+
+The demo endpoint is public. For real deployments, two options (both included as comments):
+
+- **IAM (SigV4)** - uncomment `authorizer: aws_iam` in `serverless.yml` for AWS-credentialed callers; the shared client tests this mode with `AUTH=sigv4 SERVICE=lambda`.
+- **Validate in-process** - the SDK's `requireBearerAuth` with a `jose` JWKS verifier (commented block at the bottom of `src/server.mjs`) validates OAuth Bearer tokens from any OpenID Connect provider.
+
+### Going further
+
+- **Signed round-trip state**: seal server state across elicitation retries with `createRequestStateCodec` (see the comment in `src/server.mjs`).
+- **Long-running tools**: raise the function `timeout` - streamed responses run up to 15 minutes with no other configuration.
+
+### Cleanup
+
+```bash
+serverless remove
+```
diff --git a/mcp/custom/function-url/client.mjs b/mcp/custom/function-url/client.mjs
new file mode 100644
index 000000000..7ea0a3700
--- /dev/null
+++ b/mcp/custom/function-url/client.mjs
@@ -0,0 +1,348 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the mcp/custom examples. Every example serves
+ * the same canonical MCP server, so this same script tests all of them.
+ *
+ * Usage:
+ * ENDPOINT= node client.mjs
+ * LONG=1 ENDPOINT=... node client.mjs # adds a ~36s streaming case
+ * AUTH=sigv4 SERVICE=bedrock-agentcore ENDPOINT= node client.mjs
+ * AUTH=sigv4 SERVICE=lambda ENDPOINT=... node client.mjs # IAM-auth Function URL
+ *
+ * Exercises the full 2026-07-28 surface: plain JSON, SSE progress streaming,
+ * an elicitation round-trip, resources (static + template), prompts,
+ * server/discover, cache hints, the legacy fallback, and two negative cases
+ * (header mismatch -32020, missing capability -32021).
+ */
+
+const ENDPOINT = process.env.ENDPOINT
+if (!ENDPOINT) {
+ console.error('Set ENDPOINT to the MCP endpoint URL')
+ process.exit(1)
+}
+
+// --- endpoint + optional SigV4 signing --------------------------------------
+
+let url = new URL(ENDPOINT)
+let signer
+if (process.env.AUTH === 'sigv4') {
+ const service = process.env.SERVICE || 'lambda'
+ if (service === 'bedrock-agentcore' && url.pathname.includes('/runtimes/')) {
+ // AgentCore runtime URLs embed the agent ARN - the WHOLE ARN must be
+ // URL-encoded in the path (including its inner "/").
+ const arn = decodeURIComponent(url.pathname.match(/\/runtimes\/(.+?)\/invocations/)[1])
+ url = new URL(`${url.origin}/runtimes/${encodeURIComponent(arn)}/invocations`)
+ url.searchParams.set('qualifier', 'DEFAULT')
+ }
+ const [{ SignatureV4 }, { HttpRequest }, { Sha256 }, { defaultProvider }] = await Promise.all([
+ import('@smithy/signature-v4'),
+ import('@smithy/protocol-http'),
+ import('@aws-crypto/sha256-js'),
+ import('@aws-sdk/credential-provider-node'),
+ ])
+ const sigv4 = new SignatureV4({
+ service,
+ region: process.env.AWS_REGION || 'us-east-1',
+ credentials: defaultProvider(),
+ sha256: Sha256,
+ })
+ signer = async (method, headers, body) => {
+ const request = new HttpRequest({
+ method,
+ protocol: url.protocol,
+ hostname: url.hostname,
+ path: url.pathname,
+ query: Object.fromEntries(url.searchParams),
+ headers: { host: url.hostname, ...headers },
+ ...(body != null && { body }),
+ })
+ return (await sigv4.sign(request)).headers
+ }
+}
+
+// --- request helpers ---------------------------------------------------------
+
+const CAPS_ELICIT = { elicitation: { form: {} } }
+const meta = (capabilities = {}) => ({
+ 'io.modelcontextprotocol/protocolVersion': '2026-07-28',
+ 'io.modelcontextprotocol/clientInfo': { name: 'aws-mcp-servers-client', version: '1.0.0' },
+ 'io.modelcontextprotocol/clientCapabilities': capabilities,
+})
+
+let id = 0
+async function request({ method, params = {}, name, capabilities, headerMethod, envelope = true }) {
+ const body = JSON.stringify({
+ jsonrpc: '2.0',
+ id: ++id,
+ method,
+ params: envelope ? { ...params, _meta: { ...meta(capabilities), ...(params._meta ?? {}) } } : params,
+ })
+ let headers = {
+ 'content-type': 'application/json',
+ accept: 'application/json, text/event-stream',
+ ...(envelope && {
+ 'mcp-protocol-version': '2026-07-28',
+ 'mcp-method': headerMethod ?? method,
+ ...(name && { 'mcp-name': name }),
+ }),
+ }
+ if (signer) headers = await signer('POST', headers, body)
+ const t0 = Date.now()
+ const res = await fetch(url, { method: 'POST', headers, body })
+ const contentType = res.headers.get('content-type') ?? ''
+ if (contentType.includes('text/event-stream')) {
+ const events = []
+ const decoder = new TextDecoder()
+ let buffer = ''
+ for await (const chunk of res.body) {
+ buffer += decoder.decode(chunk, { stream: true })
+ let sep
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
+ const frame = buffer.slice(0, sep)
+ buffer = buffer.slice(sep + 2)
+ const data = frame.split('\n').find((l) => l.startsWith('data:'))?.slice(5).trim()
+ if (data) events.push({ t: Date.now() - t0, data: JSON.parse(data) })
+ }
+ }
+ const final = events.at(-1)?.data
+ return { status: res.status, contentType, events, json: final, ms: Date.now() - t0 }
+ }
+ const text = await res.text()
+ let json
+ try {
+ json = JSON.parse(text)
+ } catch {
+ json = { raw: text }
+ }
+ return { status: res.status, contentType, events: [], json, ms: Date.now() - t0 }
+}
+
+// --- tiny harness ------------------------------------------------------------
+
+const results = []
+async function check(label, fn) {
+ try {
+ const detail = await fn()
+ results.push({ label, ok: true })
+ console.log(`PASS ${label}${detail ? ` — ${detail}` : ''}`)
+ } catch (err) {
+ results.push({ label, ok: false })
+ console.log(`FAIL ${label} — ${err.message}`)
+ }
+}
+const assert = (cond, msg) => {
+ if (!cond) throw new Error(msg)
+}
+// AgentCore Runtime replaces 4xx response bodies with its own error envelope
+// (-32010, "Received error (400) ..."); the 400 status still proves the SDK
+// rejected the request, so the negative cases accept that form there.
+const rejectedWith = (json, code) =>
+ json.error?.code === code ||
+ (process.env.SERVICE === 'bedrock-agentcore' &&
+ json.error?.code === -32010 &&
+ /\(4\d\d\)/.test(json.error?.message ?? ''))
+
+// --- test cases ----------------------------------------------------------------
+
+await check('1. tools/list returns the canonical tools with cache hints', async () => {
+ const r = await request({ method: 'tools/list' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ const names = (r.json.result?.tools ?? []).map((t) => t.name).sort()
+ assert(JSON.stringify(names) === JSON.stringify(['add', 'approve_refund', 'slow_report']), `tools: ${names}`)
+ assert(r.json.result.ttlMs === 300000, `ttlMs ${r.json.result.ttlMs}`)
+ assert(r.json.result.cacheScope === 'public', `cacheScope ${r.json.result.cacheScope}`)
+ return `ttlMs=${r.json.result.ttlMs} cacheScope=${r.json.result.cacheScope}`
+})
+
+await check('2. add returns plain JSON with structured content', async () => {
+ const r = await request({ method: 'tools/call', params: { name: 'add', arguments: { a: 2, b: 40 } }, name: 'add' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ assert(r.json.result?.structuredContent?.sum === 42, JSON.stringify(r.json))
+ return 'sum=42'
+})
+
+await check('3. slow_report streams incremental progress over SSE', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 3 }, _meta: { progressToken: 'pt' } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('text/event-stream'), `content-type ${r.contentType}`)
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 3, `${progress.length} progress events`)
+ assert(progress[0].t < r.ms - 500, 'first progress event did not arrive before the final result')
+ assert(r.json.result?.content?.[0]?.text === 'completed 3 steps', JSON.stringify(r.json))
+ return `3 progress events, first at +${progress[0].t}ms, done in ${r.ms}ms`
+})
+
+await check('4. approve_refund elicitation round-trip (accept and cancel)', async () => {
+ const ask = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-1' } },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(ask.json.result?.resultType === 'input_required', JSON.stringify(ask.json))
+ assert(ask.json.result?.inputRequests?.confirm, 'missing inputRequests.confirm')
+ const accepted = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: true } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(accepted.json.result?.content?.[0]?.text === 'refunded o-1', JSON.stringify(accepted.json))
+ const cancelled = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: false } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(cancelled.json.result?.content?.[0]?.text === 'refund cancelled', JSON.stringify(cancelled.json))
+ // A real decline ACTION, not accept-with-false: a server missing the
+ // inputResponse action check re-asks on decline, and a real client loops.
+ const declined = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'decline' } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(declined.json.result?.content?.[0]?.text === 'refund cancelled', `decline must terminate, not re-ask: ${JSON.stringify(declined.json)}`)
+ return 'input_required -> refunded o-1 / refund cancelled / decline terminates'
+})
+
+await check('5. resources list + read (Mcp-Name carries the uri)', async () => {
+ const list = await request({ method: 'resources/list' })
+ const uris = (list.json.result?.resources ?? []).map((r) => r.uri)
+ assert(uris.includes('guide://usage'), `resources: ${uris}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'guide://usage' }, name: 'guide://usage' })
+ assert(read.json.result?.contents?.[0]?.text?.startsWith('# Usage'), JSON.stringify(read.json))
+ return 'guide://usage readable'
+})
+
+await check('6. resource template read with per-resource cache hint', async () => {
+ const templates = await request({ method: 'resources/templates/list' })
+ const uriTemplates = (templates.json.result?.resourceTemplates ?? []).map((t) => t.uriTemplate)
+ assert(uriTemplates.includes('orders://{orderId}'), `templates: ${uriTemplates}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'orders://o-42' }, name: 'orders://o-42' })
+ const record = JSON.parse(read.json.result?.contents?.[0]?.text ?? '{}')
+ assert(record.orderId === 'o-42' && record.status === 'shipped', JSON.stringify(read.json))
+ assert(read.json.result.ttlMs === 60000, `ttlMs ${read.json.result.ttlMs}`)
+ return `orders://o-42 -> ${record.status}, ttlMs=${read.json.result.ttlMs}`
+})
+
+await check('7. prompts list + get', async () => {
+ const list = await request({ method: 'prompts/list' })
+ const names = (list.json.result?.prompts ?? []).map((p) => p.name)
+ assert(names.includes('summarize_order'), `prompts: ${names}`)
+ const got = await request({
+ method: 'prompts/get',
+ params: { name: 'summarize_order', arguments: { orderId: 'o-7' } },
+ name: 'summarize_order',
+ })
+ const text = got.json.result?.messages?.[0]?.content?.text ?? ''
+ assert(text.includes('o-7'), JSON.stringify(got.json))
+ return 'summarize_order fills its argument'
+})
+
+await check('8. server/discover surfaces the instructions', async () => {
+ const r = await request({ method: 'server/discover' })
+ assert((r.json.result?.instructions ?? '').includes('Demo MCP server'), JSON.stringify(r.json.result ?? r.json))
+ return 'instructions present'
+})
+
+await check('9. legacy initialize is answered on the same endpoint', async () => {
+ const r = await request({
+ method: 'initialize',
+ params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'legacy', version: '1.0.0' } },
+ envelope: false,
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ const version = r.json.result?.protocolVersion ?? ''
+ assert(version.startsWith('2025-'), JSON.stringify(r.json))
+ return `served as ${version}`
+})
+
+await check('10. GET is answered with the spec-mandated 405', async () => {
+ let headers = { accept: 'application/json, text/event-stream' }
+ if (signer) headers = await signer('GET', headers, null)
+ const res = await fetch(url, { method: 'GET', headers })
+ assert(res.status === 405, `HTTP ${res.status}`)
+ return 'HTTP 405'
+})
+
+await check('11. Mcp-Method header mismatching the body is rejected (-32020)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'add', arguments: { a: 1, b: 1 } },
+ name: 'add',
+ headerMethod: 'tools/list',
+ })
+ assert(rejectedWith(r.json, -32020), JSON.stringify(r.json))
+ return r.json.error.code === -32020 ? '-32020' : 'rejected (platform-wrapped 400)'
+})
+
+await check('12. elicitation without the client capability is rejected (-32021)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-9' } },
+ name: 'approve_refund',
+ capabilities: {},
+ })
+ assert(rejectedWith(r.json, -32021), JSON.stringify(r.json))
+ return r.json.error.code === -32021 ? '-32021' : 'rejected (platform-wrapped 400)'
+})
+
+if (process.env.LONG) {
+ await check('13. LONG: 45-step stream (~36s) survives past the 29s mark', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 }, _meta: { progressToken: 'long' } },
+ name: 'slow_report',
+ })
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 45, `${progress.length} progress events`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', 'final result missing (stream cut?)')
+ assert(r.ms > 34000, `finished suspiciously fast (${r.ms}ms)`)
+ return `45 events over ${(r.ms / 1000).toFixed(1)}s, final result received`
+ })
+}
+
+if (process.env.LONG) {
+ await check('14. LONG: a silent 36s call is not cut short', async () => {
+ // slow_report emits progress only when given a progressToken, so this call
+ // writes nothing at all for ~36s and then returns plain JSON. That is the
+ // case an edge-optimized API Gateway endpoint kills at 30s (its idle
+ // timeout starts at invoke, not at the first byte, and a raised
+ // timeoutInMillis does not help) - which is why the REST-fronted examples
+ // deploy on a regional endpoint.
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status} after ${(r.ms / 1000).toFixed(1)}s`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', JSON.stringify(r.json).slice(0, 200))
+ return `plain JSON after ${(r.ms / 1000).toFixed(1)}s of silence`
+ })
+}
+
+// --- summary -------------------------------------------------------------------
+
+const failed = results.filter((r) => !r.ok)
+console.log(`\n${results.length - failed.length}/${results.length} passed`)
+process.exit(failed.length ? 1 : 0)
diff --git a/mcp/custom/function-url/package-lock.json b/mcp/custom/function-url/package-lock.json
new file mode 100644
index 000000000..2ae0e613b
--- /dev/null
+++ b/mcp/custom/function-url/package-lock.json
@@ -0,0 +1,94 @@
+{
+ "name": "aws-mcp-function-url",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "aws-mcp-function-url",
+ "version": "1.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/node": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
+ "zod": "^4.2.0"
+ }
+ },
+ "node_modules/@hono/node-server": {
+ "version": "1.19.17",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz",
+ "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
+ "node_modules/@modelcontextprotocol/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/node": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz",
+ "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "hono": "^4.11.4"
+ },
+ "peerDependenciesMeta": {
+ "hono": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@modelcontextprotocol/server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/core": "2.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/hono": {
+ "version": "4.12.32",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz",
+ "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/mcp/custom/function-url/package.json b/mcp/custom/function-url/package.json
new file mode 100644
index 000000000..0aa958249
--- /dev/null
+++ b/mcp/custom/function-url/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "aws-mcp-function-url",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "MCP server on a Lambda Function URL with response streaming - no API Gateway",
+ "dependencies": {
+ "@modelcontextprotocol/node": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
+ "zod": "^4.2.0"
+ }
+}
diff --git a/mcp/custom/function-url/serverless.yml b/mcp/custom/function-url/serverless.yml
new file mode 100644
index 000000000..508bda4e3
--- /dev/null
+++ b/mcp/custom/function-url/serverless.yml
@@ -0,0 +1,26 @@
+# MCP server on a Lambda Function URL with response streaming.
+#
+# The leanest possible MCP hosting on AWS: no API Gateway, no per-request
+# fee - the Function URL invokes the Lambda directly, and response streaming
+# carries both plain JSON and SSE on the same endpoint.
+service: aws-mcp-function-url
+
+provider:
+ name: aws
+ runtime: nodejs22.x # the MCP SDK v2 requires Node.js 20+
+
+functions:
+ mcp:
+ handler: src/lambda.handler
+ timeout: 120 # streamed responses can run up to the full function timeout
+ url:
+ invokeMode: RESPONSE_STREAM
+ # Function URLs support two auth modes. The default (no authorizer) is a
+ # public endpoint - fine for trying the example, but add auth for real
+ # deployments:
+ # - IAM (SigV4) for AWS-credentialed callers (the shared client tests
+ # this with AUTH=sigv4 SERVICE=lambda):
+ # authorizer: aws_iam
+ # - or OAuth for MCP clients (Claude, AI IDEs) validated in-process with
+ # the SDK's requireBearerAuth - see the commented block in
+ # src/server.mjs.
diff --git a/mcp/custom/function-url/src/lambda.mjs b/mcp/custom/function-url/src/lambda.mjs
new file mode 100644
index 000000000..b15e1c686
--- /dev/null
+++ b/mcp/custom/function-url/src/lambda.mjs
@@ -0,0 +1,69 @@
+/**
+ * Lambda Function URL adapter for a web-standard MCP handler.
+ *
+ * The MCP SDK's serving entry is a web-standard fetch handler; its
+ * @modelcontextprotocol/node package converts Node-style request/response
+ * objects to that interface using structural (duck-typed) shapes. A Function
+ * URL invocation with response streaming provides exactly such shapes: the
+ * HTTP v2 event maps to the request side, and the awslambda response stream
+ * maps to the response side. This file is that glue - no MCP logic.
+ */
+import { toNodeHandler } from '@modelcontextprotocol/node'
+import mcpHandler from './server.mjs'
+
+const node = toNodeHandler(mcpHandler)
+
+/** Function URL (HTTP API v2.0) event -> Node-style request */
+function toNodeRequest(event) {
+ const headers = {}
+ for (const [k, v] of Object.entries(event.headers ?? {})) headers[k.toLowerCase()] = v
+ if (event.cookies?.length) headers.cookie = event.cookies.join('; ')
+ const body =
+ event.body == null ? null : Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')
+ return {
+ method: event.requestContext.http.method,
+ url: event.rawPath + (event.rawQueryString ? `?${event.rawQueryString}` : ''),
+ headers,
+ async *[Symbol.asyncIterator]() {
+ if (body) yield body
+ },
+ }
+}
+
+/** awslambda response stream -> Node-style response */
+function toNodeResponse(rawStream) {
+ let target = null // set once the handler calls writeHead
+ const pendingListeners = []
+ return {
+ get destroyed() {
+ return (target ?? rawStream).destroyed === true
+ },
+ writeHead(statusCode, headersRecord = {}) {
+ target = awslambda.HttpResponseStream.from(rawStream, { statusCode, headers: headersRecord })
+ for (const [event, listener] of pendingListeners) target.on(event, listener)
+ return this
+ },
+ write(chunk) {
+ return target.write(chunk) // real booleans keep backpressure working
+ },
+ end(chunk) {
+ return target ? target.end(chunk) : rawStream.end(chunk)
+ },
+ on(event, listener) {
+ ;(target ?? rawStream).on(event, listener)
+ if (!target) pendingListeners.push([event, listener])
+ return this
+ },
+ }
+}
+
+export const handler = awslambda.streamifyResponse(async (event, responseStream) => {
+ const res = toNodeResponse(responseStream)
+ const finished = new Promise((resolve, reject) => {
+ responseStream.on('close', resolve)
+ responseStream.on('finish', resolve)
+ responseStream.on('error', reject)
+ })
+ await node(toNodeRequest(event), res)
+ await finished
+})
diff --git a/mcp/custom/function-url/src/server.mjs b/mcp/custom/function-url/src/server.mjs
new file mode 100644
index 000000000..0a8c36a1b
--- /dev/null
+++ b/mcp/custom/function-url/src/server.mjs
@@ -0,0 +1,204 @@
+/**
+ * A standard MCP server built with the official MCP TypeScript SDK v2,
+ * serving the stateless MCP 2026-07-28 protocol revision (and answering
+ * older clients through the SDK's built-in fallback on the same endpoint).
+ *
+ * Nothing in this file is Lambda-specific - the same handler runs on any
+ * web-standard host. Every example in mcp/custom/ serves this exact
+ * server; only the hosting glue around it differs.
+ *
+ * Surface:
+ * tools: add (plain JSON), slow_report (progress streaming),
+ * approve_refund (elicitation - pauses to ask the user)
+ * resources: guide://usage, orders://{orderId} (template)
+ * prompts: summarize_order
+ * extras: instructions (served via server/discover), cache hints
+ */
+import {
+ acceptedContent,
+ createMcpHandler,
+ inputRequired,
+ inputResponse,
+ McpServer,
+ ResourceTemplate,
+} from '@modelcontextprotocol/server'
+import { z } from 'zod'
+
+const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
+
+export default createMcpHandler(() => {
+ const server = new McpServer(
+ { name: 'aws-mcp-server', version: '1.0.0' },
+ {
+ // Shown to clients that probe the server/discover method:
+ instructions:
+ 'Demo MCP server. Call add for arithmetic, slow_report to watch ' +
+ 'streamed progress, approve_refund to see a tool ask the user for ' +
+ 'confirmation mid-call. Read guide://usage for a walkthrough.',
+ // Cache hints: let clients and shared caches reuse the tool list for
+ // five minutes. Without hints the SDK emits the conservative defaults
+ // (ttlMs: 0, cacheScope: private) on every cacheable result:
+ cacheHints: { 'tools/list': { ttlMs: 300000, cacheScope: 'public' } },
+ },
+ )
+
+ // A plain tool: zod schemas next to the implementation; returning
+ // structuredContent lets clients consume typed results.
+ server.registerTool(
+ 'add',
+ {
+ description: 'Add two numbers',
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
+ outputSchema: z.object({ sum: z.number() }),
+ },
+ async ({ a, b }) => {
+ const output = { sum: a + b }
+ return {
+ content: [{ type: 'text', text: String(output.sum) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ // A long-running tool emitting progress notifications. When the client
+ // requests progress (a progressToken in _meta) and accepts text/event-stream,
+ // the response is served as SSE with notifications ahead of the final result.
+ server.registerTool(
+ 'slow_report',
+ {
+ description: 'Generate a report, reporting progress along the way',
+ inputSchema: z.object({ steps: z.number().default(3) }),
+ },
+ async ({ steps }, ctx) => {
+ // Real clients request progress via `_meta.progressToken`, never as a
+ // tool argument.
+ const progressToken = ctx.mcpReq._meta?.progressToken
+ for (let i = 1; i <= steps; i++) {
+ await sleep(800)
+ if (progressToken) {
+ await ctx.mcpReq.notify({
+ method: 'notifications/progress',
+ params: { progressToken, progress: i, total: steps, message: `step ${i}` },
+ })
+ }
+ }
+ return { content: [{ type: 'text', text: `completed ${steps} steps` }] }
+ },
+ )
+
+ // Elicitation - a tool that pauses to ask the user for input. The handler
+ // returns an input_required result; the client asks the user and retries
+ // the call with the answers attached, re-entering the handler, which reads
+ // them with acceptedContent. Works statelessly - no session required.
+ server.registerTool(
+ 'approve_refund',
+ {
+ description: 'Refund an order after user confirmation',
+ inputSchema: z.object({ orderId: z.string() }),
+ },
+ async ({ orderId }, ctx) => {
+ const answer = acceptedContent(
+ ctx.mcpReq.inputResponses,
+ 'confirm',
+ z.object({ confirmed: z.boolean() }),
+ )
+ // Decline/cancel must terminate: acceptedContent returns undefined for
+ // BOTH "not asked yet" and "user said no" - without this check a decline
+ // falls through to inputRequired and the client re-prompts forever.
+ const confirmView = inputResponse(ctx.mcpReq.inputResponses, 'confirm')
+ if (confirmView.kind === 'elicit' && confirmView.action !== 'accept') {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ if (answer === undefined) {
+ return inputRequired({
+ inputRequests: {
+ confirm: inputRequired.elicit({
+ message: `Refund order ${orderId}?`,
+ requestedSchema: z.object({ confirmed: z.boolean() }),
+ }),
+ },
+ })
+ }
+ if (!answer.confirmed) {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ return { content: [{ type: 'text', text: `refunded ${orderId}` }] }
+ },
+ )
+
+ // A readable document next to the tools. Clients fetch it with
+ // resources/read (the Mcp-Name header carries the uri).
+ server.registerResource(
+ 'usage-guide',
+ 'guide://usage',
+ { description: 'How to use this server', mimeType: 'text/markdown' },
+ async (uri) => ({
+ contents: [
+ {
+ uri: uri.href,
+ text: '# Usage\n\nCall `add` for sums, `slow_report` for streamed progress, `approve_refund` for an elicitation round-trip.',
+ },
+ ],
+ }),
+ )
+
+ // A resource template - parameterized URIs resolved per request. The
+ // per-resource cacheHint overrides the server-level hints for its reads.
+ server.registerResource(
+ 'order',
+ new ResourceTemplate('orders://{orderId}', { list: undefined }),
+ { description: 'Order record by id', cacheHint: { ttlMs: 60000, cacheScope: 'private' } },
+ async (uri, { orderId }) => ({
+ contents: [{ uri: uri.href, text: JSON.stringify({ orderId, status: 'shipped' }) }],
+ }),
+ )
+
+ // A prompt template clients can list and fill in.
+ server.registerPrompt(
+ 'summarize_order',
+ {
+ description: 'Ask the model to summarize an order',
+ argsSchema: z.object({ orderId: z.string() }),
+ },
+ ({ orderId }) => ({
+ messages: [
+ {
+ role: 'user',
+ content: { type: 'text', text: `Summarize the status of order ${orderId} in one sentence.` },
+ },
+ ],
+ }),
+ )
+
+ return server
+})
+
+// ---------------------------------------------------------------------------
+// Going further (uncomment and adapt):
+// ---------------------------------------------------------------------------
+//
+// Signed round-trip state - when an elicitation round-trip must carry server
+// state the retry cannot be allowed to tamper with (a computed price, an
+// idempotency key), return it as requestState sealed with the SDK's HMAC
+// codec. Store the >= 32-byte key in Secrets Manager or SSM:
+//
+// import { createRequestStateCodec } from '@modelcontextprotocol/server'
+// const codec = createRequestStateCodec({ key: process.env.MCP_STATE_KEY, ttlSeconds: 600 })
+//
+// In-process OAuth (works identically behind every front door): the SDK ships
+// bearer-token middleware and OAuth discovery-document helpers. Wrap the
+// handler's fetch face before exporting - any OpenID Connect provider works:
+//
+// import { requireBearerAuth, oauthMetadataResponse } from '@modelcontextprotocol/server'
+// import { createRemoteJWKSet, jwtVerify } from 'jose'
+//
+// const jwks = createRemoteJWKSet(new URL('https:///.well-known/jwks.json'))
+// const verifier = {
+// async verifyAccessToken(token) {
+// const { payload } = await jwtVerify(token, jwks, {
+// issuer: 'https:///',
+// audience: 'https://',
+// })
+// return { token, clientId: payload.azp, scopes: payload.scope?.split(' ') ?? [], expiresAt: payload.exp }
+// },
+// }
diff --git a/mcp/custom/hono/README.md b/mcp/custom/hono/README.md
new file mode 100644
index 000000000..3c8cc66b8
--- /dev/null
+++ b/mcp/custom/hono/README.md
@@ -0,0 +1,73 @@
+
+
+# MCP Server with Hono - zero custom glue
+
+Run a [Model Context Protocol](https://modelcontextprotocol.io) server on AWS Lambda using **only existing, maintained packages** for the Lambda bridge: the MCP SDK's official [`@modelcontextprotocol/hono`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/middleware/hono) integration plus [Hono](https://hono.dev)'s own `streamHandle` aws-lambda adapter. The entire Lambda-specific code is a route:
+
+```js
+const app = createMcpHonoApp({ host: '0.0.0.0' })
+app.all('/mcp', (c) => mcp.fetch(c.req.raw, { parsedBody: c.get('parsedBody') }))
+export const handler = streamHandle(app)
+```
+
+`streamHandle` dispatches on the incoming event shape, so this same export works behind a Function URL (used here), API Gateway (REST stream or HTTP API), and ALB.
+
+This is one of the [custom MCP hosting](../README.md) examples - they all serve the **same canonical MCP server** (`src/server.mjs`: tools with streamed progress and an elicitation round-trip, resources, a prompt, `server/discover` instructions, cache hints) and differ only in the hosting glue, so the same test client works against every one of them. Compare [function-url](../function-url), which does the same bridging with ~80 lines of hand-written adapter and one less dependency.
+
+A note on `createMcpHonoApp`: its localhost Host/Origin validation exists to protect *locally running* servers from DNS rebinding. Behind a Function URL or API Gateway the platform already pins the Host header, so `host: '0.0.0.0'` (intentionally exposed) is the right setting - it logs a one-line warning per cold start. Access control for a cloud endpoint comes from the auth options below instead.
+
+## Usage
+
+### Deploy
+
+```bash
+npm install
+serverless deploy
+```
+
+### Test
+
+Every example in this family ships the same test client:
+
+```bash
+ENDPOINT=mcp node client.mjs
+```
+
+```
+PASS 1. tools/list returns the canonical tools with cache hints — ttlMs=300000 cacheScope=public
+PASS 2. add returns plain JSON with structured content — sum=42
+PASS 3. slow_report streams incremental progress over SSE — 3 progress events, first at +810ms
+PASS 4. approve_refund elicitation round-trip (accept and cancel) — input_required -> refunded o-1 / refund cancelled
+PASS 5. resources list + read (Mcp-Name carries the uri) — guide://usage readable
+PASS 6. resource template read with per-resource cache hint — orders://o-42 -> shipped, ttlMs=60000
+PASS 7. prompts list + get — summarize_order fills its argument
+PASS 8. server/discover surfaces the instructions — instructions present
+PASS 9. legacy initialize is answered on the same endpoint — served as 2025-06-18
+PASS 10. GET is answered with the spec-mandated 405 — HTTP 405
+PASS 11. Mcp-Method header mismatching the body is rejected (-32020) — -32020
+PASS 12. elicitation without the client capability is rejected (-32021) — -32021
+
+12/12 passed
+```
+
+Add `LONG=1` for a ~36-second streaming case.
+
+### Authentication
+
+The demo endpoint is public. For real deployments (both included as comments): uncomment `authorizer: aws_iam` in `serverless.yml` for AWS-credentialed callers (the shared client tests this mode with `AUTH=sigv4 SERVICE=lambda`), or validate OAuth Bearer tokens in-process with the SDK's `requireBearerAuth` and a `jose` JWKS verifier (commented block at the bottom of `src/server.mjs`) - works with any OpenID Connect provider.
+
+### Cleanup
+
+```bash
+serverless remove
+```
diff --git a/mcp/custom/hono/client.mjs b/mcp/custom/hono/client.mjs
new file mode 100644
index 000000000..7ea0a3700
--- /dev/null
+++ b/mcp/custom/hono/client.mjs
@@ -0,0 +1,348 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the mcp/custom examples. Every example serves
+ * the same canonical MCP server, so this same script tests all of them.
+ *
+ * Usage:
+ * ENDPOINT= node client.mjs
+ * LONG=1 ENDPOINT=... node client.mjs # adds a ~36s streaming case
+ * AUTH=sigv4 SERVICE=bedrock-agentcore ENDPOINT= node client.mjs
+ * AUTH=sigv4 SERVICE=lambda ENDPOINT=... node client.mjs # IAM-auth Function URL
+ *
+ * Exercises the full 2026-07-28 surface: plain JSON, SSE progress streaming,
+ * an elicitation round-trip, resources (static + template), prompts,
+ * server/discover, cache hints, the legacy fallback, and two negative cases
+ * (header mismatch -32020, missing capability -32021).
+ */
+
+const ENDPOINT = process.env.ENDPOINT
+if (!ENDPOINT) {
+ console.error('Set ENDPOINT to the MCP endpoint URL')
+ process.exit(1)
+}
+
+// --- endpoint + optional SigV4 signing --------------------------------------
+
+let url = new URL(ENDPOINT)
+let signer
+if (process.env.AUTH === 'sigv4') {
+ const service = process.env.SERVICE || 'lambda'
+ if (service === 'bedrock-agentcore' && url.pathname.includes('/runtimes/')) {
+ // AgentCore runtime URLs embed the agent ARN - the WHOLE ARN must be
+ // URL-encoded in the path (including its inner "/").
+ const arn = decodeURIComponent(url.pathname.match(/\/runtimes\/(.+?)\/invocations/)[1])
+ url = new URL(`${url.origin}/runtimes/${encodeURIComponent(arn)}/invocations`)
+ url.searchParams.set('qualifier', 'DEFAULT')
+ }
+ const [{ SignatureV4 }, { HttpRequest }, { Sha256 }, { defaultProvider }] = await Promise.all([
+ import('@smithy/signature-v4'),
+ import('@smithy/protocol-http'),
+ import('@aws-crypto/sha256-js'),
+ import('@aws-sdk/credential-provider-node'),
+ ])
+ const sigv4 = new SignatureV4({
+ service,
+ region: process.env.AWS_REGION || 'us-east-1',
+ credentials: defaultProvider(),
+ sha256: Sha256,
+ })
+ signer = async (method, headers, body) => {
+ const request = new HttpRequest({
+ method,
+ protocol: url.protocol,
+ hostname: url.hostname,
+ path: url.pathname,
+ query: Object.fromEntries(url.searchParams),
+ headers: { host: url.hostname, ...headers },
+ ...(body != null && { body }),
+ })
+ return (await sigv4.sign(request)).headers
+ }
+}
+
+// --- request helpers ---------------------------------------------------------
+
+const CAPS_ELICIT = { elicitation: { form: {} } }
+const meta = (capabilities = {}) => ({
+ 'io.modelcontextprotocol/protocolVersion': '2026-07-28',
+ 'io.modelcontextprotocol/clientInfo': { name: 'aws-mcp-servers-client', version: '1.0.0' },
+ 'io.modelcontextprotocol/clientCapabilities': capabilities,
+})
+
+let id = 0
+async function request({ method, params = {}, name, capabilities, headerMethod, envelope = true }) {
+ const body = JSON.stringify({
+ jsonrpc: '2.0',
+ id: ++id,
+ method,
+ params: envelope ? { ...params, _meta: { ...meta(capabilities), ...(params._meta ?? {}) } } : params,
+ })
+ let headers = {
+ 'content-type': 'application/json',
+ accept: 'application/json, text/event-stream',
+ ...(envelope && {
+ 'mcp-protocol-version': '2026-07-28',
+ 'mcp-method': headerMethod ?? method,
+ ...(name && { 'mcp-name': name }),
+ }),
+ }
+ if (signer) headers = await signer('POST', headers, body)
+ const t0 = Date.now()
+ const res = await fetch(url, { method: 'POST', headers, body })
+ const contentType = res.headers.get('content-type') ?? ''
+ if (contentType.includes('text/event-stream')) {
+ const events = []
+ const decoder = new TextDecoder()
+ let buffer = ''
+ for await (const chunk of res.body) {
+ buffer += decoder.decode(chunk, { stream: true })
+ let sep
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
+ const frame = buffer.slice(0, sep)
+ buffer = buffer.slice(sep + 2)
+ const data = frame.split('\n').find((l) => l.startsWith('data:'))?.slice(5).trim()
+ if (data) events.push({ t: Date.now() - t0, data: JSON.parse(data) })
+ }
+ }
+ const final = events.at(-1)?.data
+ return { status: res.status, contentType, events, json: final, ms: Date.now() - t0 }
+ }
+ const text = await res.text()
+ let json
+ try {
+ json = JSON.parse(text)
+ } catch {
+ json = { raw: text }
+ }
+ return { status: res.status, contentType, events: [], json, ms: Date.now() - t0 }
+}
+
+// --- tiny harness ------------------------------------------------------------
+
+const results = []
+async function check(label, fn) {
+ try {
+ const detail = await fn()
+ results.push({ label, ok: true })
+ console.log(`PASS ${label}${detail ? ` — ${detail}` : ''}`)
+ } catch (err) {
+ results.push({ label, ok: false })
+ console.log(`FAIL ${label} — ${err.message}`)
+ }
+}
+const assert = (cond, msg) => {
+ if (!cond) throw new Error(msg)
+}
+// AgentCore Runtime replaces 4xx response bodies with its own error envelope
+// (-32010, "Received error (400) ..."); the 400 status still proves the SDK
+// rejected the request, so the negative cases accept that form there.
+const rejectedWith = (json, code) =>
+ json.error?.code === code ||
+ (process.env.SERVICE === 'bedrock-agentcore' &&
+ json.error?.code === -32010 &&
+ /\(4\d\d\)/.test(json.error?.message ?? ''))
+
+// --- test cases ----------------------------------------------------------------
+
+await check('1. tools/list returns the canonical tools with cache hints', async () => {
+ const r = await request({ method: 'tools/list' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ const names = (r.json.result?.tools ?? []).map((t) => t.name).sort()
+ assert(JSON.stringify(names) === JSON.stringify(['add', 'approve_refund', 'slow_report']), `tools: ${names}`)
+ assert(r.json.result.ttlMs === 300000, `ttlMs ${r.json.result.ttlMs}`)
+ assert(r.json.result.cacheScope === 'public', `cacheScope ${r.json.result.cacheScope}`)
+ return `ttlMs=${r.json.result.ttlMs} cacheScope=${r.json.result.cacheScope}`
+})
+
+await check('2. add returns plain JSON with structured content', async () => {
+ const r = await request({ method: 'tools/call', params: { name: 'add', arguments: { a: 2, b: 40 } }, name: 'add' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ assert(r.json.result?.structuredContent?.sum === 42, JSON.stringify(r.json))
+ return 'sum=42'
+})
+
+await check('3. slow_report streams incremental progress over SSE', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 3 }, _meta: { progressToken: 'pt' } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('text/event-stream'), `content-type ${r.contentType}`)
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 3, `${progress.length} progress events`)
+ assert(progress[0].t < r.ms - 500, 'first progress event did not arrive before the final result')
+ assert(r.json.result?.content?.[0]?.text === 'completed 3 steps', JSON.stringify(r.json))
+ return `3 progress events, first at +${progress[0].t}ms, done in ${r.ms}ms`
+})
+
+await check('4. approve_refund elicitation round-trip (accept and cancel)', async () => {
+ const ask = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-1' } },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(ask.json.result?.resultType === 'input_required', JSON.stringify(ask.json))
+ assert(ask.json.result?.inputRequests?.confirm, 'missing inputRequests.confirm')
+ const accepted = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: true } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(accepted.json.result?.content?.[0]?.text === 'refunded o-1', JSON.stringify(accepted.json))
+ const cancelled = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: false } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(cancelled.json.result?.content?.[0]?.text === 'refund cancelled', JSON.stringify(cancelled.json))
+ // A real decline ACTION, not accept-with-false: a server missing the
+ // inputResponse action check re-asks on decline, and a real client loops.
+ const declined = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'decline' } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(declined.json.result?.content?.[0]?.text === 'refund cancelled', `decline must terminate, not re-ask: ${JSON.stringify(declined.json)}`)
+ return 'input_required -> refunded o-1 / refund cancelled / decline terminates'
+})
+
+await check('5. resources list + read (Mcp-Name carries the uri)', async () => {
+ const list = await request({ method: 'resources/list' })
+ const uris = (list.json.result?.resources ?? []).map((r) => r.uri)
+ assert(uris.includes('guide://usage'), `resources: ${uris}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'guide://usage' }, name: 'guide://usage' })
+ assert(read.json.result?.contents?.[0]?.text?.startsWith('# Usage'), JSON.stringify(read.json))
+ return 'guide://usage readable'
+})
+
+await check('6. resource template read with per-resource cache hint', async () => {
+ const templates = await request({ method: 'resources/templates/list' })
+ const uriTemplates = (templates.json.result?.resourceTemplates ?? []).map((t) => t.uriTemplate)
+ assert(uriTemplates.includes('orders://{orderId}'), `templates: ${uriTemplates}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'orders://o-42' }, name: 'orders://o-42' })
+ const record = JSON.parse(read.json.result?.contents?.[0]?.text ?? '{}')
+ assert(record.orderId === 'o-42' && record.status === 'shipped', JSON.stringify(read.json))
+ assert(read.json.result.ttlMs === 60000, `ttlMs ${read.json.result.ttlMs}`)
+ return `orders://o-42 -> ${record.status}, ttlMs=${read.json.result.ttlMs}`
+})
+
+await check('7. prompts list + get', async () => {
+ const list = await request({ method: 'prompts/list' })
+ const names = (list.json.result?.prompts ?? []).map((p) => p.name)
+ assert(names.includes('summarize_order'), `prompts: ${names}`)
+ const got = await request({
+ method: 'prompts/get',
+ params: { name: 'summarize_order', arguments: { orderId: 'o-7' } },
+ name: 'summarize_order',
+ })
+ const text = got.json.result?.messages?.[0]?.content?.text ?? ''
+ assert(text.includes('o-7'), JSON.stringify(got.json))
+ return 'summarize_order fills its argument'
+})
+
+await check('8. server/discover surfaces the instructions', async () => {
+ const r = await request({ method: 'server/discover' })
+ assert((r.json.result?.instructions ?? '').includes('Demo MCP server'), JSON.stringify(r.json.result ?? r.json))
+ return 'instructions present'
+})
+
+await check('9. legacy initialize is answered on the same endpoint', async () => {
+ const r = await request({
+ method: 'initialize',
+ params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'legacy', version: '1.0.0' } },
+ envelope: false,
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ const version = r.json.result?.protocolVersion ?? ''
+ assert(version.startsWith('2025-'), JSON.stringify(r.json))
+ return `served as ${version}`
+})
+
+await check('10. GET is answered with the spec-mandated 405', async () => {
+ let headers = { accept: 'application/json, text/event-stream' }
+ if (signer) headers = await signer('GET', headers, null)
+ const res = await fetch(url, { method: 'GET', headers })
+ assert(res.status === 405, `HTTP ${res.status}`)
+ return 'HTTP 405'
+})
+
+await check('11. Mcp-Method header mismatching the body is rejected (-32020)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'add', arguments: { a: 1, b: 1 } },
+ name: 'add',
+ headerMethod: 'tools/list',
+ })
+ assert(rejectedWith(r.json, -32020), JSON.stringify(r.json))
+ return r.json.error.code === -32020 ? '-32020' : 'rejected (platform-wrapped 400)'
+})
+
+await check('12. elicitation without the client capability is rejected (-32021)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-9' } },
+ name: 'approve_refund',
+ capabilities: {},
+ })
+ assert(rejectedWith(r.json, -32021), JSON.stringify(r.json))
+ return r.json.error.code === -32021 ? '-32021' : 'rejected (platform-wrapped 400)'
+})
+
+if (process.env.LONG) {
+ await check('13. LONG: 45-step stream (~36s) survives past the 29s mark', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 }, _meta: { progressToken: 'long' } },
+ name: 'slow_report',
+ })
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 45, `${progress.length} progress events`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', 'final result missing (stream cut?)')
+ assert(r.ms > 34000, `finished suspiciously fast (${r.ms}ms)`)
+ return `45 events over ${(r.ms / 1000).toFixed(1)}s, final result received`
+ })
+}
+
+if (process.env.LONG) {
+ await check('14. LONG: a silent 36s call is not cut short', async () => {
+ // slow_report emits progress only when given a progressToken, so this call
+ // writes nothing at all for ~36s and then returns plain JSON. That is the
+ // case an edge-optimized API Gateway endpoint kills at 30s (its idle
+ // timeout starts at invoke, not at the first byte, and a raised
+ // timeoutInMillis does not help) - which is why the REST-fronted examples
+ // deploy on a regional endpoint.
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status} after ${(r.ms / 1000).toFixed(1)}s`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', JSON.stringify(r.json).slice(0, 200))
+ return `plain JSON after ${(r.ms / 1000).toFixed(1)}s of silence`
+ })
+}
+
+// --- summary -------------------------------------------------------------------
+
+const failed = results.filter((r) => !r.ok)
+console.log(`\n${results.length - failed.length}/${results.length} passed`)
+process.exit(failed.length ? 1 : 0)
diff --git a/mcp/custom/hono/package-lock.json b/mcp/custom/hono/package-lock.json
new file mode 100644
index 000000000..5844571a1
--- /dev/null
+++ b/mcp/custom/hono/package-lock.json
@@ -0,0 +1,74 @@
+{
+ "name": "aws-mcp-hono",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "aws-mcp-hono",
+ "version": "1.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/hono": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
+ "hono": "^4.11.0",
+ "zod": "^4.2.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/hono": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/hono/-/hono-2.0.0.tgz",
+ "integrity": "sha512-lKCSXRusLxpX63G+6og9ewcRkqVbjB2xSxpvU/VJhxcUkpvGgpDrmcgNRQ4dTapGr8AspNFdRBdmJc500Vhl/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "hono": "^4.11.4"
+ }
+ },
+ "node_modules/@modelcontextprotocol/server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/core": "2.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/hono": {
+ "version": "4.12.32",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz",
+ "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/mcp/custom/hono/package.json b/mcp/custom/hono/package.json
new file mode 100644
index 000000000..267b19683
--- /dev/null
+++ b/mcp/custom/hono/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "aws-mcp-hono",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "MCP server on AWS Lambda via the official MCP Hono integration and Hono's aws-lambda adapter - zero custom glue",
+ "dependencies": {
+ "@modelcontextprotocol/hono": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
+ "hono": "^4.11.0",
+ "zod": "^4.2.0"
+ }
+}
diff --git a/mcp/custom/hono/serverless.yml b/mcp/custom/hono/serverless.yml
new file mode 100644
index 000000000..9e1906861
--- /dev/null
+++ b/mcp/custom/hono/serverless.yml
@@ -0,0 +1,23 @@
+# MCP server on Lambda via Hono's aws-lambda adapter - zero custom glue.
+#
+# The MCP SDK's official Hono integration plus Hono's own streamHandle do all
+# the bridging; src/lambda.mjs is just a route. Served on a Function URL with
+# response streaming.
+service: aws-mcp-hono
+
+provider:
+ name: aws
+ runtime: nodejs22.x # the MCP SDK v2 requires Node.js 20+
+
+functions:
+ mcp:
+ handler: src/lambda.handler
+ timeout: 120 # streamed responses can run up to the full function timeout
+ url:
+ invokeMode: RESPONSE_STREAM
+ # The default (no authorizer) is a public endpoint - fine for trying the
+ # example, but add auth for real deployments:
+ # - IAM (SigV4) for AWS-credentialed callers:
+ # authorizer: aws_iam
+ # - or OAuth validated in-process with the SDK's requireBearerAuth - see
+ # the commented block in src/server.mjs.
diff --git a/mcp/custom/hono/src/lambda.mjs b/mcp/custom/hono/src/lambda.mjs
new file mode 100644
index 000000000..76b35e920
--- /dev/null
+++ b/mcp/custom/hono/src/lambda.mjs
@@ -0,0 +1,22 @@
+/**
+ * The whole Lambda bridge - no hand-written event or stream mapping:
+ * Hono's own aws-lambda adapter (streamHandle) converts Lambda events to
+ * web-standard Requests and pumps the Response into the Lambda response
+ * stream, and the MCP handler's fetch face plugs straight into a route.
+ * streamHandle dispatches on the event shape, so the same export works
+ * behind a Function URL, API Gateway (REST or HTTP API), and ALB.
+ */
+import { createMcpHonoApp } from '@modelcontextprotocol/hono'
+import { streamHandle } from 'hono/aws-lambda'
+import mcp from './server.mjs'
+
+// host: '0.0.0.0' declares this app as intentionally exposed, which disables
+// the factory's localhost DNS-rebinding protections - correct behind a cloud
+// front door, where the platform already pins the Host header. (It logs a
+// one-line warning per cold start.) Serving the same app locally or behind a
+// custom domain? Use createMcpHonoApp({ allowedHosts: ['api.example.com'] }).
+const app = createMcpHonoApp({ host: '0.0.0.0' })
+
+app.all('/mcp', (c) => mcp.fetch(c.req.raw, { parsedBody: c.get('parsedBody') }))
+
+export const handler = streamHandle(app)
diff --git a/mcp/custom/hono/src/server.mjs b/mcp/custom/hono/src/server.mjs
new file mode 100644
index 000000000..0a8c36a1b
--- /dev/null
+++ b/mcp/custom/hono/src/server.mjs
@@ -0,0 +1,204 @@
+/**
+ * A standard MCP server built with the official MCP TypeScript SDK v2,
+ * serving the stateless MCP 2026-07-28 protocol revision (and answering
+ * older clients through the SDK's built-in fallback on the same endpoint).
+ *
+ * Nothing in this file is Lambda-specific - the same handler runs on any
+ * web-standard host. Every example in mcp/custom/ serves this exact
+ * server; only the hosting glue around it differs.
+ *
+ * Surface:
+ * tools: add (plain JSON), slow_report (progress streaming),
+ * approve_refund (elicitation - pauses to ask the user)
+ * resources: guide://usage, orders://{orderId} (template)
+ * prompts: summarize_order
+ * extras: instructions (served via server/discover), cache hints
+ */
+import {
+ acceptedContent,
+ createMcpHandler,
+ inputRequired,
+ inputResponse,
+ McpServer,
+ ResourceTemplate,
+} from '@modelcontextprotocol/server'
+import { z } from 'zod'
+
+const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
+
+export default createMcpHandler(() => {
+ const server = new McpServer(
+ { name: 'aws-mcp-server', version: '1.0.0' },
+ {
+ // Shown to clients that probe the server/discover method:
+ instructions:
+ 'Demo MCP server. Call add for arithmetic, slow_report to watch ' +
+ 'streamed progress, approve_refund to see a tool ask the user for ' +
+ 'confirmation mid-call. Read guide://usage for a walkthrough.',
+ // Cache hints: let clients and shared caches reuse the tool list for
+ // five minutes. Without hints the SDK emits the conservative defaults
+ // (ttlMs: 0, cacheScope: private) on every cacheable result:
+ cacheHints: { 'tools/list': { ttlMs: 300000, cacheScope: 'public' } },
+ },
+ )
+
+ // A plain tool: zod schemas next to the implementation; returning
+ // structuredContent lets clients consume typed results.
+ server.registerTool(
+ 'add',
+ {
+ description: 'Add two numbers',
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
+ outputSchema: z.object({ sum: z.number() }),
+ },
+ async ({ a, b }) => {
+ const output = { sum: a + b }
+ return {
+ content: [{ type: 'text', text: String(output.sum) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ // A long-running tool emitting progress notifications. When the client
+ // requests progress (a progressToken in _meta) and accepts text/event-stream,
+ // the response is served as SSE with notifications ahead of the final result.
+ server.registerTool(
+ 'slow_report',
+ {
+ description: 'Generate a report, reporting progress along the way',
+ inputSchema: z.object({ steps: z.number().default(3) }),
+ },
+ async ({ steps }, ctx) => {
+ // Real clients request progress via `_meta.progressToken`, never as a
+ // tool argument.
+ const progressToken = ctx.mcpReq._meta?.progressToken
+ for (let i = 1; i <= steps; i++) {
+ await sleep(800)
+ if (progressToken) {
+ await ctx.mcpReq.notify({
+ method: 'notifications/progress',
+ params: { progressToken, progress: i, total: steps, message: `step ${i}` },
+ })
+ }
+ }
+ return { content: [{ type: 'text', text: `completed ${steps} steps` }] }
+ },
+ )
+
+ // Elicitation - a tool that pauses to ask the user for input. The handler
+ // returns an input_required result; the client asks the user and retries
+ // the call with the answers attached, re-entering the handler, which reads
+ // them with acceptedContent. Works statelessly - no session required.
+ server.registerTool(
+ 'approve_refund',
+ {
+ description: 'Refund an order after user confirmation',
+ inputSchema: z.object({ orderId: z.string() }),
+ },
+ async ({ orderId }, ctx) => {
+ const answer = acceptedContent(
+ ctx.mcpReq.inputResponses,
+ 'confirm',
+ z.object({ confirmed: z.boolean() }),
+ )
+ // Decline/cancel must terminate: acceptedContent returns undefined for
+ // BOTH "not asked yet" and "user said no" - without this check a decline
+ // falls through to inputRequired and the client re-prompts forever.
+ const confirmView = inputResponse(ctx.mcpReq.inputResponses, 'confirm')
+ if (confirmView.kind === 'elicit' && confirmView.action !== 'accept') {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ if (answer === undefined) {
+ return inputRequired({
+ inputRequests: {
+ confirm: inputRequired.elicit({
+ message: `Refund order ${orderId}?`,
+ requestedSchema: z.object({ confirmed: z.boolean() }),
+ }),
+ },
+ })
+ }
+ if (!answer.confirmed) {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ return { content: [{ type: 'text', text: `refunded ${orderId}` }] }
+ },
+ )
+
+ // A readable document next to the tools. Clients fetch it with
+ // resources/read (the Mcp-Name header carries the uri).
+ server.registerResource(
+ 'usage-guide',
+ 'guide://usage',
+ { description: 'How to use this server', mimeType: 'text/markdown' },
+ async (uri) => ({
+ contents: [
+ {
+ uri: uri.href,
+ text: '# Usage\n\nCall `add` for sums, `slow_report` for streamed progress, `approve_refund` for an elicitation round-trip.',
+ },
+ ],
+ }),
+ )
+
+ // A resource template - parameterized URIs resolved per request. The
+ // per-resource cacheHint overrides the server-level hints for its reads.
+ server.registerResource(
+ 'order',
+ new ResourceTemplate('orders://{orderId}', { list: undefined }),
+ { description: 'Order record by id', cacheHint: { ttlMs: 60000, cacheScope: 'private' } },
+ async (uri, { orderId }) => ({
+ contents: [{ uri: uri.href, text: JSON.stringify({ orderId, status: 'shipped' }) }],
+ }),
+ )
+
+ // A prompt template clients can list and fill in.
+ server.registerPrompt(
+ 'summarize_order',
+ {
+ description: 'Ask the model to summarize an order',
+ argsSchema: z.object({ orderId: z.string() }),
+ },
+ ({ orderId }) => ({
+ messages: [
+ {
+ role: 'user',
+ content: { type: 'text', text: `Summarize the status of order ${orderId} in one sentence.` },
+ },
+ ],
+ }),
+ )
+
+ return server
+})
+
+// ---------------------------------------------------------------------------
+// Going further (uncomment and adapt):
+// ---------------------------------------------------------------------------
+//
+// Signed round-trip state - when an elicitation round-trip must carry server
+// state the retry cannot be allowed to tamper with (a computed price, an
+// idempotency key), return it as requestState sealed with the SDK's HMAC
+// codec. Store the >= 32-byte key in Secrets Manager or SSM:
+//
+// import { createRequestStateCodec } from '@modelcontextprotocol/server'
+// const codec = createRequestStateCodec({ key: process.env.MCP_STATE_KEY, ttlSeconds: 600 })
+//
+// In-process OAuth (works identically behind every front door): the SDK ships
+// bearer-token middleware and OAuth discovery-document helpers. Wrap the
+// handler's fetch face before exporting - any OpenID Connect provider works:
+//
+// import { requireBearerAuth, oauthMetadataResponse } from '@modelcontextprotocol/server'
+// import { createRemoteJWKSet, jwtVerify } from 'jose'
+//
+// const jwks = createRemoteJWKSet(new URL('https:///.well-known/jwks.json'))
+// const verifier = {
+// async verifyAccessToken(token) {
+// const { payload } = await jwtVerify(token, jwks, {
+// issuer: 'https:///',
+// audience: 'https://',
+// })
+// return { token, clientId: payload.azp, scopes: payload.scope?.split(' ') ?? [], expiresAt: payload.exp }
+// },
+// }
diff --git a/mcp/custom/rest-api/README.md b/mcp/custom/rest-api/README.md
new file mode 100644
index 000000000..73983b4b3
--- /dev/null
+++ b/mcp/custom/rest-api/README.md
@@ -0,0 +1,78 @@
+
+
+# MCP Server behind API Gateway REST (response streaming)
+
+Run a [Model Context Protocol](https://modelcontextprotocol.io) server built with the **official [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)** on AWS Lambda behind an API Gateway REST API in stream mode. Pick this front door when you want a custom domain, WAF, throttling, usage plans, or an authorizer that rejects requests before they invoke the function.
+
+This is one of the [custom MCP hosting](../README.md) examples - they all serve the **same canonical MCP server** and differ only in the hosting glue, so the same test client works against every one of them. Three parts:
+
+- **`src/server.mjs`** - the canonical server: tools (`add`, `slow_report` with streamed progress, `approve_refund` with an elicitation round-trip), resources (`guide://usage` plus an `orders://{orderId}` template), a prompt, `instructions` served via `server/discover`, and cache hints on the tool list. Nothing Lambda-specific - the same file runs on any web-standard host.
+- **`src/lambda.mjs`** - ~80 lines of adapter glue mapping the API Gateway event and the Lambda response stream onto the Node-style shapes that `@modelcontextprotocol/node` accepts. No MCP logic. (For glue-free alternatives built on existing packages, see the [hono](../hono) and [express-web-adapter](../express-web-adapter) examples.)
+- **`serverless.yml`** - a REST API with `response.transferMode: stream` and a raised `timeoutInMillis` (streams end at the integration timeout, so lift it alongside).
+
+Plain JSON and streaming SSE coexist on the one endpoint: simple tool calls return ordinary `application/json`, while calls that request progress stream `text/event-stream` with notifications arriving ahead of the final result.
+
+## Usage
+
+### Deploy
+
+```bash
+npm install
+serverless deploy
+```
+
+### Test
+
+Every example in this family ships the same test client:
+
+```bash
+ENDPOINT= node client.mjs
+```
+
+```
+PASS 1. tools/list returns the canonical tools with cache hints — ttlMs=300000 cacheScope=public
+PASS 2. add returns plain JSON with structured content — sum=42
+PASS 3. slow_report streams incremental progress over SSE — 3 progress events, first at +810ms
+PASS 4. approve_refund elicitation round-trip (accept and cancel) — input_required -> refunded o-1 / refund cancelled
+PASS 5. resources list + read (Mcp-Name carries the uri) — guide://usage readable
+PASS 6. resource template read with per-resource cache hint — orders://o-42 -> shipped, ttlMs=60000
+PASS 7. prompts list + get — summarize_order fills its argument
+PASS 8. server/discover surfaces the instructions — instructions present
+PASS 9. legacy initialize is answered on the same endpoint — served as 2025-06-18
+PASS 10. GET is answered with the spec-mandated 405 — HTTP 405
+PASS 11. Mcp-Method header mismatching the body is rejected (-32020) — -32020
+PASS 12. elicitation without the client capability is rejected (-32021) — -32021
+
+12/12 passed
+```
+
+Add `LONG=1` for a ~36-second streaming case proving streams run far past API Gateway's classic 29-second ceiling.
+
+### Authentication
+
+The demo endpoint is public. For real deployments, two options (both included):
+
+- **Reject at the gateway** - uncomment the `authorizer` wiring in `serverless.yml`: [src/authorizer.mjs](src/authorizer.mjs) is a complete Lambda authorizer validating Bearer JWTs against any OpenID Connect provider, so unauthenticated requests never invoke (or bill) the MCP function.
+- **Validate in-process** - the SDK's `requireBearerAuth` with a `jose` JWKS verifier (commented block at the bottom of `src/server.mjs`); works identically behind every front door, at the cost of unauthenticated requests still invoking the function.
+
+### Going further
+
+- **Signed round-trip state**: seal server state across elicitation retries with `createRequestStateCodec` (see the comment in `src/server.mjs`).
+- **OAuth discovery**: serve `/.well-known/oauth-protected-resource` so MCP clients can discover your authorization server (commented route in `serverless.yml`).
+- **Long-running tools**: raise the function `timeout` and `timeoutInMillis` - streamed responses run up to 15 minutes. Keep the endpoint regional (as configured): edge-optimized REST endpoints cut streams that stay idle for 30 seconds, so a tool that computes quietly for longer than that would fail there even with a raised timeout.
+
+### Cleanup
+
+```bash
+serverless remove
+```
diff --git a/mcp/custom/rest-api/client.mjs b/mcp/custom/rest-api/client.mjs
new file mode 100644
index 000000000..7ea0a3700
--- /dev/null
+++ b/mcp/custom/rest-api/client.mjs
@@ -0,0 +1,348 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the mcp/custom examples. Every example serves
+ * the same canonical MCP server, so this same script tests all of them.
+ *
+ * Usage:
+ * ENDPOINT= node client.mjs
+ * LONG=1 ENDPOINT=... node client.mjs # adds a ~36s streaming case
+ * AUTH=sigv4 SERVICE=bedrock-agentcore ENDPOINT= node client.mjs
+ * AUTH=sigv4 SERVICE=lambda ENDPOINT=... node client.mjs # IAM-auth Function URL
+ *
+ * Exercises the full 2026-07-28 surface: plain JSON, SSE progress streaming,
+ * an elicitation round-trip, resources (static + template), prompts,
+ * server/discover, cache hints, the legacy fallback, and two negative cases
+ * (header mismatch -32020, missing capability -32021).
+ */
+
+const ENDPOINT = process.env.ENDPOINT
+if (!ENDPOINT) {
+ console.error('Set ENDPOINT to the MCP endpoint URL')
+ process.exit(1)
+}
+
+// --- endpoint + optional SigV4 signing --------------------------------------
+
+let url = new URL(ENDPOINT)
+let signer
+if (process.env.AUTH === 'sigv4') {
+ const service = process.env.SERVICE || 'lambda'
+ if (service === 'bedrock-agentcore' && url.pathname.includes('/runtimes/')) {
+ // AgentCore runtime URLs embed the agent ARN - the WHOLE ARN must be
+ // URL-encoded in the path (including its inner "/").
+ const arn = decodeURIComponent(url.pathname.match(/\/runtimes\/(.+?)\/invocations/)[1])
+ url = new URL(`${url.origin}/runtimes/${encodeURIComponent(arn)}/invocations`)
+ url.searchParams.set('qualifier', 'DEFAULT')
+ }
+ const [{ SignatureV4 }, { HttpRequest }, { Sha256 }, { defaultProvider }] = await Promise.all([
+ import('@smithy/signature-v4'),
+ import('@smithy/protocol-http'),
+ import('@aws-crypto/sha256-js'),
+ import('@aws-sdk/credential-provider-node'),
+ ])
+ const sigv4 = new SignatureV4({
+ service,
+ region: process.env.AWS_REGION || 'us-east-1',
+ credentials: defaultProvider(),
+ sha256: Sha256,
+ })
+ signer = async (method, headers, body) => {
+ const request = new HttpRequest({
+ method,
+ protocol: url.protocol,
+ hostname: url.hostname,
+ path: url.pathname,
+ query: Object.fromEntries(url.searchParams),
+ headers: { host: url.hostname, ...headers },
+ ...(body != null && { body }),
+ })
+ return (await sigv4.sign(request)).headers
+ }
+}
+
+// --- request helpers ---------------------------------------------------------
+
+const CAPS_ELICIT = { elicitation: { form: {} } }
+const meta = (capabilities = {}) => ({
+ 'io.modelcontextprotocol/protocolVersion': '2026-07-28',
+ 'io.modelcontextprotocol/clientInfo': { name: 'aws-mcp-servers-client', version: '1.0.0' },
+ 'io.modelcontextprotocol/clientCapabilities': capabilities,
+})
+
+let id = 0
+async function request({ method, params = {}, name, capabilities, headerMethod, envelope = true }) {
+ const body = JSON.stringify({
+ jsonrpc: '2.0',
+ id: ++id,
+ method,
+ params: envelope ? { ...params, _meta: { ...meta(capabilities), ...(params._meta ?? {}) } } : params,
+ })
+ let headers = {
+ 'content-type': 'application/json',
+ accept: 'application/json, text/event-stream',
+ ...(envelope && {
+ 'mcp-protocol-version': '2026-07-28',
+ 'mcp-method': headerMethod ?? method,
+ ...(name && { 'mcp-name': name }),
+ }),
+ }
+ if (signer) headers = await signer('POST', headers, body)
+ const t0 = Date.now()
+ const res = await fetch(url, { method: 'POST', headers, body })
+ const contentType = res.headers.get('content-type') ?? ''
+ if (contentType.includes('text/event-stream')) {
+ const events = []
+ const decoder = new TextDecoder()
+ let buffer = ''
+ for await (const chunk of res.body) {
+ buffer += decoder.decode(chunk, { stream: true })
+ let sep
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
+ const frame = buffer.slice(0, sep)
+ buffer = buffer.slice(sep + 2)
+ const data = frame.split('\n').find((l) => l.startsWith('data:'))?.slice(5).trim()
+ if (data) events.push({ t: Date.now() - t0, data: JSON.parse(data) })
+ }
+ }
+ const final = events.at(-1)?.data
+ return { status: res.status, contentType, events, json: final, ms: Date.now() - t0 }
+ }
+ const text = await res.text()
+ let json
+ try {
+ json = JSON.parse(text)
+ } catch {
+ json = { raw: text }
+ }
+ return { status: res.status, contentType, events: [], json, ms: Date.now() - t0 }
+}
+
+// --- tiny harness ------------------------------------------------------------
+
+const results = []
+async function check(label, fn) {
+ try {
+ const detail = await fn()
+ results.push({ label, ok: true })
+ console.log(`PASS ${label}${detail ? ` — ${detail}` : ''}`)
+ } catch (err) {
+ results.push({ label, ok: false })
+ console.log(`FAIL ${label} — ${err.message}`)
+ }
+}
+const assert = (cond, msg) => {
+ if (!cond) throw new Error(msg)
+}
+// AgentCore Runtime replaces 4xx response bodies with its own error envelope
+// (-32010, "Received error (400) ..."); the 400 status still proves the SDK
+// rejected the request, so the negative cases accept that form there.
+const rejectedWith = (json, code) =>
+ json.error?.code === code ||
+ (process.env.SERVICE === 'bedrock-agentcore' &&
+ json.error?.code === -32010 &&
+ /\(4\d\d\)/.test(json.error?.message ?? ''))
+
+// --- test cases ----------------------------------------------------------------
+
+await check('1. tools/list returns the canonical tools with cache hints', async () => {
+ const r = await request({ method: 'tools/list' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ const names = (r.json.result?.tools ?? []).map((t) => t.name).sort()
+ assert(JSON.stringify(names) === JSON.stringify(['add', 'approve_refund', 'slow_report']), `tools: ${names}`)
+ assert(r.json.result.ttlMs === 300000, `ttlMs ${r.json.result.ttlMs}`)
+ assert(r.json.result.cacheScope === 'public', `cacheScope ${r.json.result.cacheScope}`)
+ return `ttlMs=${r.json.result.ttlMs} cacheScope=${r.json.result.cacheScope}`
+})
+
+await check('2. add returns plain JSON with structured content', async () => {
+ const r = await request({ method: 'tools/call', params: { name: 'add', arguments: { a: 2, b: 40 } }, name: 'add' })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('application/json'), `content-type ${r.contentType}`)
+ assert(r.json.result?.structuredContent?.sum === 42, JSON.stringify(r.json))
+ return 'sum=42'
+})
+
+await check('3. slow_report streams incremental progress over SSE', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 3 }, _meta: { progressToken: 'pt' } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ assert(r.contentType.includes('text/event-stream'), `content-type ${r.contentType}`)
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 3, `${progress.length} progress events`)
+ assert(progress[0].t < r.ms - 500, 'first progress event did not arrive before the final result')
+ assert(r.json.result?.content?.[0]?.text === 'completed 3 steps', JSON.stringify(r.json))
+ return `3 progress events, first at +${progress[0].t}ms, done in ${r.ms}ms`
+})
+
+await check('4. approve_refund elicitation round-trip (accept and cancel)', async () => {
+ const ask = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-1' } },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(ask.json.result?.resultType === 'input_required', JSON.stringify(ask.json))
+ assert(ask.json.result?.inputRequests?.confirm, 'missing inputRequests.confirm')
+ const accepted = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: true } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(accepted.json.result?.content?.[0]?.text === 'refunded o-1', JSON.stringify(accepted.json))
+ const cancelled = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'accept', content: { confirmed: false } } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(cancelled.json.result?.content?.[0]?.text === 'refund cancelled', JSON.stringify(cancelled.json))
+ // A real decline ACTION, not accept-with-false: a server missing the
+ // inputResponse action check re-asks on decline, and a real client loops.
+ const declined = await request({
+ method: 'tools/call',
+ params: {
+ name: 'approve_refund',
+ arguments: { orderId: 'o-1' },
+ inputResponses: { confirm: { action: 'decline' } },
+ },
+ name: 'approve_refund',
+ capabilities: CAPS_ELICIT,
+ })
+ assert(declined.json.result?.content?.[0]?.text === 'refund cancelled', `decline must terminate, not re-ask: ${JSON.stringify(declined.json)}`)
+ return 'input_required -> refunded o-1 / refund cancelled / decline terminates'
+})
+
+await check('5. resources list + read (Mcp-Name carries the uri)', async () => {
+ const list = await request({ method: 'resources/list' })
+ const uris = (list.json.result?.resources ?? []).map((r) => r.uri)
+ assert(uris.includes('guide://usage'), `resources: ${uris}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'guide://usage' }, name: 'guide://usage' })
+ assert(read.json.result?.contents?.[0]?.text?.startsWith('# Usage'), JSON.stringify(read.json))
+ return 'guide://usage readable'
+})
+
+await check('6. resource template read with per-resource cache hint', async () => {
+ const templates = await request({ method: 'resources/templates/list' })
+ const uriTemplates = (templates.json.result?.resourceTemplates ?? []).map((t) => t.uriTemplate)
+ assert(uriTemplates.includes('orders://{orderId}'), `templates: ${uriTemplates}`)
+ const read = await request({ method: 'resources/read', params: { uri: 'orders://o-42' }, name: 'orders://o-42' })
+ const record = JSON.parse(read.json.result?.contents?.[0]?.text ?? '{}')
+ assert(record.orderId === 'o-42' && record.status === 'shipped', JSON.stringify(read.json))
+ assert(read.json.result.ttlMs === 60000, `ttlMs ${read.json.result.ttlMs}`)
+ return `orders://o-42 -> ${record.status}, ttlMs=${read.json.result.ttlMs}`
+})
+
+await check('7. prompts list + get', async () => {
+ const list = await request({ method: 'prompts/list' })
+ const names = (list.json.result?.prompts ?? []).map((p) => p.name)
+ assert(names.includes('summarize_order'), `prompts: ${names}`)
+ const got = await request({
+ method: 'prompts/get',
+ params: { name: 'summarize_order', arguments: { orderId: 'o-7' } },
+ name: 'summarize_order',
+ })
+ const text = got.json.result?.messages?.[0]?.content?.text ?? ''
+ assert(text.includes('o-7'), JSON.stringify(got.json))
+ return 'summarize_order fills its argument'
+})
+
+await check('8. server/discover surfaces the instructions', async () => {
+ const r = await request({ method: 'server/discover' })
+ assert((r.json.result?.instructions ?? '').includes('Demo MCP server'), JSON.stringify(r.json.result ?? r.json))
+ return 'instructions present'
+})
+
+await check('9. legacy initialize is answered on the same endpoint', async () => {
+ const r = await request({
+ method: 'initialize',
+ params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'legacy', version: '1.0.0' } },
+ envelope: false,
+ })
+ assert(r.status === 200, `HTTP ${r.status}`)
+ const version = r.json.result?.protocolVersion ?? ''
+ assert(version.startsWith('2025-'), JSON.stringify(r.json))
+ return `served as ${version}`
+})
+
+await check('10. GET is answered with the spec-mandated 405', async () => {
+ let headers = { accept: 'application/json, text/event-stream' }
+ if (signer) headers = await signer('GET', headers, null)
+ const res = await fetch(url, { method: 'GET', headers })
+ assert(res.status === 405, `HTTP ${res.status}`)
+ return 'HTTP 405'
+})
+
+await check('11. Mcp-Method header mismatching the body is rejected (-32020)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'add', arguments: { a: 1, b: 1 } },
+ name: 'add',
+ headerMethod: 'tools/list',
+ })
+ assert(rejectedWith(r.json, -32020), JSON.stringify(r.json))
+ return r.json.error.code === -32020 ? '-32020' : 'rejected (platform-wrapped 400)'
+})
+
+await check('12. elicitation without the client capability is rejected (-32021)', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'approve_refund', arguments: { orderId: 'o-9' } },
+ name: 'approve_refund',
+ capabilities: {},
+ })
+ assert(rejectedWith(r.json, -32021), JSON.stringify(r.json))
+ return r.json.error.code === -32021 ? '-32021' : 'rejected (platform-wrapped 400)'
+})
+
+if (process.env.LONG) {
+ await check('13. LONG: 45-step stream (~36s) survives past the 29s mark', async () => {
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 }, _meta: { progressToken: 'long' } },
+ name: 'slow_report',
+ })
+ const progress = r.events.filter((e) => e.data.method === 'notifications/progress')
+ assert(progress.length === 45, `${progress.length} progress events`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', 'final result missing (stream cut?)')
+ assert(r.ms > 34000, `finished suspiciously fast (${r.ms}ms)`)
+ return `45 events over ${(r.ms / 1000).toFixed(1)}s, final result received`
+ })
+}
+
+if (process.env.LONG) {
+ await check('14. LONG: a silent 36s call is not cut short', async () => {
+ // slow_report emits progress only when given a progressToken, so this call
+ // writes nothing at all for ~36s and then returns plain JSON. That is the
+ // case an edge-optimized API Gateway endpoint kills at 30s (its idle
+ // timeout starts at invoke, not at the first byte, and a raised
+ // timeoutInMillis does not help) - which is why the REST-fronted examples
+ // deploy on a regional endpoint.
+ const r = await request({
+ method: 'tools/call',
+ params: { name: 'slow_report', arguments: { steps: 45 } },
+ name: 'slow_report',
+ })
+ assert(r.status === 200, `HTTP ${r.status} after ${(r.ms / 1000).toFixed(1)}s`)
+ assert(r.json.result?.content?.[0]?.text === 'completed 45 steps', JSON.stringify(r.json).slice(0, 200))
+ return `plain JSON after ${(r.ms / 1000).toFixed(1)}s of silence`
+ })
+}
+
+// --- summary -------------------------------------------------------------------
+
+const failed = results.filter((r) => !r.ok)
+console.log(`\n${results.length - failed.length}/${results.length} passed`)
+process.exit(failed.length ? 1 : 0)
diff --git a/mcp/custom/rest-api/package-lock.json b/mcp/custom/rest-api/package-lock.json
new file mode 100644
index 000000000..bb281ae2a
--- /dev/null
+++ b/mcp/custom/rest-api/package-lock.json
@@ -0,0 +1,104 @@
+{
+ "name": "aws-mcp-rest-api",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "aws-mcp-rest-api",
+ "version": "1.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/node": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
+ "jose": "^6.0.0",
+ "zod": "^4.2.0"
+ }
+ },
+ "node_modules/@hono/node-server": {
+ "version": "1.19.17",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz",
+ "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
+ "node_modules/@modelcontextprotocol/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/node": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz",
+ "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "hono": "^4.11.4"
+ },
+ "peerDependenciesMeta": {
+ "hono": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@modelcontextprotocol/server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/core": "2.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/hono": {
+ "version": "4.12.32",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz",
+ "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/jose": {
+ "version": "6.2.4",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz",
+ "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/mcp/custom/rest-api/package.json b/mcp/custom/rest-api/package.json
new file mode 100644
index 000000000..6e6e56b5f
--- /dev/null
+++ b/mcp/custom/rest-api/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "aws-mcp-rest-api",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "MCP server on AWS Lambda with the official MCP SDK and API Gateway response streaming",
+ "dependencies": {
+ "@modelcontextprotocol/node": "^2.0.0",
+ "@modelcontextprotocol/server": "^2.0.0",
+ "jose": "^6.0.0",
+ "zod": "^4.2.0"
+ }
+}
diff --git a/mcp/custom/rest-api/serverless.yml b/mcp/custom/rest-api/serverless.yml
new file mode 100644
index 000000000..db1b45da0
--- /dev/null
+++ b/mcp/custom/rest-api/serverless.yml
@@ -0,0 +1,57 @@
+# MCP server on AWS Lambda, built with the official MCP TypeScript SDK v2
+# and served through API Gateway REST response streaming.
+#
+# Plain JSON responses and streaming SSE responses both work through the same
+# endpoint - the server picks per request.
+service: aws-mcp-rest-api
+
+provider:
+ name: aws
+ runtime: nodejs22.x # the MCP SDK v2 requires Node.js 20+
+ # Regional endpoint, not the default edge-optimized one: edge REST endpoints
+ # cut idle streams after 30 seconds, which kills a tool that computes quietly
+ # for longer than that before responding. Regional raises that bound to
+ # 5 minutes (verified: an identical 36s silent call returns 200 on regional
+ # and 504 on edge).
+ endpointType: REGIONAL
+
+functions:
+ mcp:
+ handler: src/lambda.handler
+ timeout: 120
+ events:
+ - http:
+ method: POST
+ path: /mcp
+ # Response streaming lifts API Gateway's classic response limits and
+ # enables SSE, but the integration timeout must be raised explicitly
+ # alongside it (up to 15 minutes):
+ timeoutInMillis: 120000
+ response:
+ transferMode: stream
+ # To reject unauthenticated requests before they invoke the function,
+ # attach the JWT authorizer below (src/authorizer.mjs validates
+ # Bearer tokens against any OpenID Connect provider), or use the
+ # SDK's in-process bearer auth (see src/server.mjs):
+ # authorizer:
+ # name: jwtAuthorizer
+ - http:
+ method: GET
+ path: /mcp # the server answers the spec-mandated 405 for GET
+ response:
+ transferMode: stream # every route on a streaming handler streams
+
+ # OAuth discovery for MCP clients ("paste URL and log in"): serve the
+ # protected-resource metadata on an unauthenticated route. The SDK's
+ # oauthMetadata middleware can generate the document (see src/server.mjs):
+ # - http:
+ # method: GET
+ # path: /.well-known/oauth-protected-resource
+
+ # The request authorizer pairing with the `authorizer` block above:
+ # jwtAuthorizer:
+ # handler: src/authorizer.handler
+ # environment:
+ # JWKS_URL: https:///.well-known/jwks.json
+ # TOKEN_ISSUER: https:///
+ # TOKEN_AUDIENCE: https://
diff --git a/mcp/custom/rest-api/src/authorizer.mjs b/mcp/custom/rest-api/src/authorizer.mjs
new file mode 100644
index 000000000..6e451fced
--- /dev/null
+++ b/mcp/custom/rest-api/src/authorizer.mjs
@@ -0,0 +1,33 @@
+/**
+ * A Lambda TOKEN authorizer validating OAuth Bearer JWTs against any OpenID
+ * Connect provider - API Gateway rejects unauthenticated requests before they
+ * ever invoke (and bill) the MCP function.
+ *
+ * Configure via environment variables on the authorizer function:
+ * JWKS_URL e.g. https://your-tenant.example.com/.well-known/jwks.json
+ * TOKEN_ISSUER e.g. https://your-tenant.example.com/
+ * TOKEN_AUDIENCE e.g. https://your-mcp-endpoint
+ */
+import { createRemoteJWKSet, jwtVerify } from 'jose'
+
+const jwks = createRemoteJWKSet(new URL(process.env.JWKS_URL))
+
+export const handler = async (event) => {
+ const token = event.authorizationToken?.replace(/^Bearer /i, '')
+ try {
+ const { payload } = await jwtVerify(token, jwks, {
+ issuer: process.env.TOKEN_ISSUER,
+ audience: process.env.TOKEN_AUDIENCE,
+ })
+ return {
+ principalId: payload.sub,
+ policyDocument: {
+ Version: '2012-10-17',
+ Statement: [{ Action: 'execute-api:Invoke', Effect: 'Allow', Resource: event.methodArn }],
+ },
+ }
+ } catch {
+ // API Gateway maps this exact message to a 401 response.
+ throw new Error('Unauthorized')
+ }
+}
diff --git a/mcp/custom/rest-api/src/lambda.mjs b/mcp/custom/rest-api/src/lambda.mjs
new file mode 100644
index 000000000..372c1d1f5
--- /dev/null
+++ b/mcp/custom/rest-api/src/lambda.mjs
@@ -0,0 +1,75 @@
+/**
+ * Lambda adapter for a web-standard MCP handler.
+ *
+ * The MCP SDK's serving entry is a web-standard fetch handler, and its
+ * @modelcontextprotocol/node package converts Node-style request/response
+ * objects to that interface using structural (duck-typed) shapes. Lambda's
+ * response streaming gives us exactly such shapes: the API Gateway event maps
+ * to the request side, and the awslambda response stream maps to the response
+ * side. This file is that glue - it contains no MCP-specific logic.
+ */
+import { toNodeHandler } from '@modelcontextprotocol/node'
+import mcpHandler from './server.mjs'
+
+const node = toNodeHandler(mcpHandler)
+
+/** API Gateway (REST, streaming mode) proxy event -> Node-style request */
+function toNodeRequest(event) {
+ const headers = {}
+ for (const [k, v] of Object.entries(event.multiValueHeaders ?? {})) {
+ headers[k.toLowerCase()] = Array.isArray(v) && v.length === 1 ? v[0] : v
+ }
+ const qs = event.multiValueQueryStringParameters
+ ? new URLSearchParams(
+ Object.entries(event.multiValueQueryStringParameters).flatMap(([k, vs]) => vs.map((v) => [k, v])),
+ ).toString()
+ : ''
+ const body =
+ event.body == null ? null : Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')
+ return {
+ method: event.httpMethod,
+ url: event.path + (qs ? `?${qs}` : ''),
+ headers,
+ async *[Symbol.asyncIterator]() {
+ if (body) yield body
+ },
+ }
+}
+
+/** awslambda response stream -> Node-style response */
+function toNodeResponse(rawStream) {
+ let target = null // set once the handler calls writeHead
+ const pendingListeners = []
+ return {
+ get destroyed() {
+ return (target ?? rawStream).destroyed === true
+ },
+ writeHead(statusCode, headersRecord = {}) {
+ target = awslambda.HttpResponseStream.from(rawStream, { statusCode, headers: headersRecord })
+ for (const [event, listener] of pendingListeners) target.on(event, listener)
+ return this
+ },
+ write(chunk) {
+ return target.write(chunk) // real booleans keep backpressure working
+ },
+ end(chunk) {
+ return target ? target.end(chunk) : rawStream.end(chunk)
+ },
+ on(event, listener) {
+ ;(target ?? rawStream).on(event, listener)
+ if (!target) pendingListeners.push([event, listener])
+ return this
+ },
+ }
+}
+
+export const handler = awslambda.streamifyResponse(async (event, responseStream) => {
+ const res = toNodeResponse(responseStream)
+ const finished = new Promise((resolve, reject) => {
+ responseStream.on('close', resolve)
+ responseStream.on('finish', resolve)
+ responseStream.on('error', reject)
+ })
+ await node(toNodeRequest(event), res)
+ await finished
+})
diff --git a/mcp/custom/rest-api/src/server.mjs b/mcp/custom/rest-api/src/server.mjs
new file mode 100644
index 000000000..0a8c36a1b
--- /dev/null
+++ b/mcp/custom/rest-api/src/server.mjs
@@ -0,0 +1,204 @@
+/**
+ * A standard MCP server built with the official MCP TypeScript SDK v2,
+ * serving the stateless MCP 2026-07-28 protocol revision (and answering
+ * older clients through the SDK's built-in fallback on the same endpoint).
+ *
+ * Nothing in this file is Lambda-specific - the same handler runs on any
+ * web-standard host. Every example in mcp/custom/ serves this exact
+ * server; only the hosting glue around it differs.
+ *
+ * Surface:
+ * tools: add (plain JSON), slow_report (progress streaming),
+ * approve_refund (elicitation - pauses to ask the user)
+ * resources: guide://usage, orders://{orderId} (template)
+ * prompts: summarize_order
+ * extras: instructions (served via server/discover), cache hints
+ */
+import {
+ acceptedContent,
+ createMcpHandler,
+ inputRequired,
+ inputResponse,
+ McpServer,
+ ResourceTemplate,
+} from '@modelcontextprotocol/server'
+import { z } from 'zod'
+
+const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
+
+export default createMcpHandler(() => {
+ const server = new McpServer(
+ { name: 'aws-mcp-server', version: '1.0.0' },
+ {
+ // Shown to clients that probe the server/discover method:
+ instructions:
+ 'Demo MCP server. Call add for arithmetic, slow_report to watch ' +
+ 'streamed progress, approve_refund to see a tool ask the user for ' +
+ 'confirmation mid-call. Read guide://usage for a walkthrough.',
+ // Cache hints: let clients and shared caches reuse the tool list for
+ // five minutes. Without hints the SDK emits the conservative defaults
+ // (ttlMs: 0, cacheScope: private) on every cacheable result:
+ cacheHints: { 'tools/list': { ttlMs: 300000, cacheScope: 'public' } },
+ },
+ )
+
+ // A plain tool: zod schemas next to the implementation; returning
+ // structuredContent lets clients consume typed results.
+ server.registerTool(
+ 'add',
+ {
+ description: 'Add two numbers',
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
+ outputSchema: z.object({ sum: z.number() }),
+ },
+ async ({ a, b }) => {
+ const output = { sum: a + b }
+ return {
+ content: [{ type: 'text', text: String(output.sum) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ // A long-running tool emitting progress notifications. When the client
+ // requests progress (a progressToken in _meta) and accepts text/event-stream,
+ // the response is served as SSE with notifications ahead of the final result.
+ server.registerTool(
+ 'slow_report',
+ {
+ description: 'Generate a report, reporting progress along the way',
+ inputSchema: z.object({ steps: z.number().default(3) }),
+ },
+ async ({ steps }, ctx) => {
+ // Real clients request progress via `_meta.progressToken`, never as a
+ // tool argument.
+ const progressToken = ctx.mcpReq._meta?.progressToken
+ for (let i = 1; i <= steps; i++) {
+ await sleep(800)
+ if (progressToken) {
+ await ctx.mcpReq.notify({
+ method: 'notifications/progress',
+ params: { progressToken, progress: i, total: steps, message: `step ${i}` },
+ })
+ }
+ }
+ return { content: [{ type: 'text', text: `completed ${steps} steps` }] }
+ },
+ )
+
+ // Elicitation - a tool that pauses to ask the user for input. The handler
+ // returns an input_required result; the client asks the user and retries
+ // the call with the answers attached, re-entering the handler, which reads
+ // them with acceptedContent. Works statelessly - no session required.
+ server.registerTool(
+ 'approve_refund',
+ {
+ description: 'Refund an order after user confirmation',
+ inputSchema: z.object({ orderId: z.string() }),
+ },
+ async ({ orderId }, ctx) => {
+ const answer = acceptedContent(
+ ctx.mcpReq.inputResponses,
+ 'confirm',
+ z.object({ confirmed: z.boolean() }),
+ )
+ // Decline/cancel must terminate: acceptedContent returns undefined for
+ // BOTH "not asked yet" and "user said no" - without this check a decline
+ // falls through to inputRequired and the client re-prompts forever.
+ const confirmView = inputResponse(ctx.mcpReq.inputResponses, 'confirm')
+ if (confirmView.kind === 'elicit' && confirmView.action !== 'accept') {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ if (answer === undefined) {
+ return inputRequired({
+ inputRequests: {
+ confirm: inputRequired.elicit({
+ message: `Refund order ${orderId}?`,
+ requestedSchema: z.object({ confirmed: z.boolean() }),
+ }),
+ },
+ })
+ }
+ if (!answer.confirmed) {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ return { content: [{ type: 'text', text: `refunded ${orderId}` }] }
+ },
+ )
+
+ // A readable document next to the tools. Clients fetch it with
+ // resources/read (the Mcp-Name header carries the uri).
+ server.registerResource(
+ 'usage-guide',
+ 'guide://usage',
+ { description: 'How to use this server', mimeType: 'text/markdown' },
+ async (uri) => ({
+ contents: [
+ {
+ uri: uri.href,
+ text: '# Usage\n\nCall `add` for sums, `slow_report` for streamed progress, `approve_refund` for an elicitation round-trip.',
+ },
+ ],
+ }),
+ )
+
+ // A resource template - parameterized URIs resolved per request. The
+ // per-resource cacheHint overrides the server-level hints for its reads.
+ server.registerResource(
+ 'order',
+ new ResourceTemplate('orders://{orderId}', { list: undefined }),
+ { description: 'Order record by id', cacheHint: { ttlMs: 60000, cacheScope: 'private' } },
+ async (uri, { orderId }) => ({
+ contents: [{ uri: uri.href, text: JSON.stringify({ orderId, status: 'shipped' }) }],
+ }),
+ )
+
+ // A prompt template clients can list and fill in.
+ server.registerPrompt(
+ 'summarize_order',
+ {
+ description: 'Ask the model to summarize an order',
+ argsSchema: z.object({ orderId: z.string() }),
+ },
+ ({ orderId }) => ({
+ messages: [
+ {
+ role: 'user',
+ content: { type: 'text', text: `Summarize the status of order ${orderId} in one sentence.` },
+ },
+ ],
+ }),
+ )
+
+ return server
+})
+
+// ---------------------------------------------------------------------------
+// Going further (uncomment and adapt):
+// ---------------------------------------------------------------------------
+//
+// Signed round-trip state - when an elicitation round-trip must carry server
+// state the retry cannot be allowed to tamper with (a computed price, an
+// idempotency key), return it as requestState sealed with the SDK's HMAC
+// codec. Store the >= 32-byte key in Secrets Manager or SSM:
+//
+// import { createRequestStateCodec } from '@modelcontextprotocol/server'
+// const codec = createRequestStateCodec({ key: process.env.MCP_STATE_KEY, ttlSeconds: 600 })
+//
+// In-process OAuth (works identically behind every front door): the SDK ships
+// bearer-token middleware and OAuth discovery-document helpers. Wrap the
+// handler's fetch face before exporting - any OpenID Connect provider works:
+//
+// import { requireBearerAuth, oauthMetadataResponse } from '@modelcontextprotocol/server'
+// import { createRemoteJWKSet, jwtVerify } from 'jose'
+//
+// const jwks = createRemoteJWKSet(new URL('https:///.well-known/jwks.json'))
+// const verifier = {
+// async verifyAccessToken(token) {
+// const { payload } = await jwtVerify(token, jwks, {
+// issuer: 'https:///',
+// audience: 'https://',
+// })
+// return { token, clientId: payload.azp, scopes: payload.scope?.split(' ') ?? [], expiresAt: payload.exp }
+// },
+// }
diff --git a/mcp/custom/server.mjs b/mcp/custom/server.mjs
new file mode 100644
index 000000000..0a8c36a1b
--- /dev/null
+++ b/mcp/custom/server.mjs
@@ -0,0 +1,204 @@
+/**
+ * A standard MCP server built with the official MCP TypeScript SDK v2,
+ * serving the stateless MCP 2026-07-28 protocol revision (and answering
+ * older clients through the SDK's built-in fallback on the same endpoint).
+ *
+ * Nothing in this file is Lambda-specific - the same handler runs on any
+ * web-standard host. Every example in mcp/custom/ serves this exact
+ * server; only the hosting glue around it differs.
+ *
+ * Surface:
+ * tools: add (plain JSON), slow_report (progress streaming),
+ * approve_refund (elicitation - pauses to ask the user)
+ * resources: guide://usage, orders://{orderId} (template)
+ * prompts: summarize_order
+ * extras: instructions (served via server/discover), cache hints
+ */
+import {
+ acceptedContent,
+ createMcpHandler,
+ inputRequired,
+ inputResponse,
+ McpServer,
+ ResourceTemplate,
+} from '@modelcontextprotocol/server'
+import { z } from 'zod'
+
+const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
+
+export default createMcpHandler(() => {
+ const server = new McpServer(
+ { name: 'aws-mcp-server', version: '1.0.0' },
+ {
+ // Shown to clients that probe the server/discover method:
+ instructions:
+ 'Demo MCP server. Call add for arithmetic, slow_report to watch ' +
+ 'streamed progress, approve_refund to see a tool ask the user for ' +
+ 'confirmation mid-call. Read guide://usage for a walkthrough.',
+ // Cache hints: let clients and shared caches reuse the tool list for
+ // five minutes. Without hints the SDK emits the conservative defaults
+ // (ttlMs: 0, cacheScope: private) on every cacheable result:
+ cacheHints: { 'tools/list': { ttlMs: 300000, cacheScope: 'public' } },
+ },
+ )
+
+ // A plain tool: zod schemas next to the implementation; returning
+ // structuredContent lets clients consume typed results.
+ server.registerTool(
+ 'add',
+ {
+ description: 'Add two numbers',
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
+ outputSchema: z.object({ sum: z.number() }),
+ },
+ async ({ a, b }) => {
+ const output = { sum: a + b }
+ return {
+ content: [{ type: 'text', text: String(output.sum) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ // A long-running tool emitting progress notifications. When the client
+ // requests progress (a progressToken in _meta) and accepts text/event-stream,
+ // the response is served as SSE with notifications ahead of the final result.
+ server.registerTool(
+ 'slow_report',
+ {
+ description: 'Generate a report, reporting progress along the way',
+ inputSchema: z.object({ steps: z.number().default(3) }),
+ },
+ async ({ steps }, ctx) => {
+ // Real clients request progress via `_meta.progressToken`, never as a
+ // tool argument.
+ const progressToken = ctx.mcpReq._meta?.progressToken
+ for (let i = 1; i <= steps; i++) {
+ await sleep(800)
+ if (progressToken) {
+ await ctx.mcpReq.notify({
+ method: 'notifications/progress',
+ params: { progressToken, progress: i, total: steps, message: `step ${i}` },
+ })
+ }
+ }
+ return { content: [{ type: 'text', text: `completed ${steps} steps` }] }
+ },
+ )
+
+ // Elicitation - a tool that pauses to ask the user for input. The handler
+ // returns an input_required result; the client asks the user and retries
+ // the call with the answers attached, re-entering the handler, which reads
+ // them with acceptedContent. Works statelessly - no session required.
+ server.registerTool(
+ 'approve_refund',
+ {
+ description: 'Refund an order after user confirmation',
+ inputSchema: z.object({ orderId: z.string() }),
+ },
+ async ({ orderId }, ctx) => {
+ const answer = acceptedContent(
+ ctx.mcpReq.inputResponses,
+ 'confirm',
+ z.object({ confirmed: z.boolean() }),
+ )
+ // Decline/cancel must terminate: acceptedContent returns undefined for
+ // BOTH "not asked yet" and "user said no" - without this check a decline
+ // falls through to inputRequired and the client re-prompts forever.
+ const confirmView = inputResponse(ctx.mcpReq.inputResponses, 'confirm')
+ if (confirmView.kind === 'elicit' && confirmView.action !== 'accept') {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ if (answer === undefined) {
+ return inputRequired({
+ inputRequests: {
+ confirm: inputRequired.elicit({
+ message: `Refund order ${orderId}?`,
+ requestedSchema: z.object({ confirmed: z.boolean() }),
+ }),
+ },
+ })
+ }
+ if (!answer.confirmed) {
+ return { content: [{ type: 'text', text: 'refund cancelled' }] }
+ }
+ return { content: [{ type: 'text', text: `refunded ${orderId}` }] }
+ },
+ )
+
+ // A readable document next to the tools. Clients fetch it with
+ // resources/read (the Mcp-Name header carries the uri).
+ server.registerResource(
+ 'usage-guide',
+ 'guide://usage',
+ { description: 'How to use this server', mimeType: 'text/markdown' },
+ async (uri) => ({
+ contents: [
+ {
+ uri: uri.href,
+ text: '# Usage\n\nCall `add` for sums, `slow_report` for streamed progress, `approve_refund` for an elicitation round-trip.',
+ },
+ ],
+ }),
+ )
+
+ // A resource template - parameterized URIs resolved per request. The
+ // per-resource cacheHint overrides the server-level hints for its reads.
+ server.registerResource(
+ 'order',
+ new ResourceTemplate('orders://{orderId}', { list: undefined }),
+ { description: 'Order record by id', cacheHint: { ttlMs: 60000, cacheScope: 'private' } },
+ async (uri, { orderId }) => ({
+ contents: [{ uri: uri.href, text: JSON.stringify({ orderId, status: 'shipped' }) }],
+ }),
+ )
+
+ // A prompt template clients can list and fill in.
+ server.registerPrompt(
+ 'summarize_order',
+ {
+ description: 'Ask the model to summarize an order',
+ argsSchema: z.object({ orderId: z.string() }),
+ },
+ ({ orderId }) => ({
+ messages: [
+ {
+ role: 'user',
+ content: { type: 'text', text: `Summarize the status of order ${orderId} in one sentence.` },
+ },
+ ],
+ }),
+ )
+
+ return server
+})
+
+// ---------------------------------------------------------------------------
+// Going further (uncomment and adapt):
+// ---------------------------------------------------------------------------
+//
+// Signed round-trip state - when an elicitation round-trip must carry server
+// state the retry cannot be allowed to tamper with (a computed price, an
+// idempotency key), return it as requestState sealed with the SDK's HMAC
+// codec. Store the >= 32-byte key in Secrets Manager or SSM:
+//
+// import { createRequestStateCodec } from '@modelcontextprotocol/server'
+// const codec = createRequestStateCodec({ key: process.env.MCP_STATE_KEY, ttlSeconds: 600 })
+//
+// In-process OAuth (works identically behind every front door): the SDK ships
+// bearer-token middleware and OAuth discovery-document helpers. Wrap the
+// handler's fetch face before exporting - any OpenID Connect provider works:
+//
+// import { requireBearerAuth, oauthMetadataResponse } from '@modelcontextprotocol/server'
+// import { createRemoteJWKSet, jwtVerify } from 'jose'
+//
+// const jwks = createRemoteJWKSet(new URL('https:///.well-known/jwks.json'))
+// const verifier = {
+// async verifyAccessToken(token) {
+// const { payload } = await jwtVerify(token, jwks, {
+// issuer: 'https:///',
+// audience: 'https://',
+// })
+// return { token, clientId: payload.azp, scopes: payload.scope?.split(' ') ?? [], expiresAt: payload.exp }
+// },
+// }
diff --git a/mcp/minimal/README.md b/mcp/minimal/README.md
new file mode 100644
index 000000000..7a19352ab
--- /dev/null
+++ b/mcp/minimal/README.md
@@ -0,0 +1,180 @@
+
+
+# MCP Server — minimal
+
+The smallest deployable member of the [MCP examples](../README.md): no auth, no domain — the [oauth-cognito](../oauth-cognito), [oauth-authorizer](../oauth-authorizer), and [oauth-in-module](../oauth-in-module) examples each add enforcement in one of its three shapes, and the hub README carries the client-capability matrix that applies to all of them.
+
+Deploy a [Model Context Protocol](https://modelcontextprotocol.io) server, written against the official [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk), with the Serverless Framework's [built-in MCP server support](https://www.serverless.com/framework/docs/providers/aws/guide/mcp).
+
+There are two files. `src/server.mjs` is a plain SDK server with no Lambda concepts and no Framework APIs in it. `serverless.yml` names it:
+
+```yaml
+mcp:
+ servers:
+ demo:
+ server: src/server.mjs
+```
+
+That is the whole integration. The Framework owns everything around the module: the HTTPS endpoint at `/demo/mcp` on an API Gateway REST API in streaming mode, the integration timeout kept in step with the function timeout, packaging, and the prebuilt Lambda entry that bridges Lambda's streaming runtime to the SDK handler's web-standard `fetch`. The server becomes an ordinary function in the service model, so `serverless logs -f demo`, metrics, versions, and rollback work with no special casing.
+
+The server exposes two tools, which is enough to see both response modes:
+
+- **`add`** — returns immediately, as plain JSON with structured output.
+- **`slow_report`** — works for a while and emits progress notifications, which stream back as `text/event-stream` ahead of the final result.
+
+## Deployment
+
+```bash
+npm install
+serverless deploy
+```
+
+The deploy summary prints the endpoint, and `serverless info` prints it again later:
+
+```
+mcp: demo → https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp
+```
+
+The `dev` segment tracks the stage, so a `--stage` deploy changes every URL below with it. Deploying again without any changes prints `No changes to deploy. Deployment skipped.` — `--force` deploys anyway.
+
+## Testing
+
+Any Streamable HTTP MCP client works. The MCP Inspector's CLI mode takes a config file (`mcp.json` below) naming the endpoint. `"protocolEra": "modern"` opts it into the current protocol revision (its default is an older one) — this server answers without it too; the opt-in starts to matter once a tool depends on the current revision, as elicitation does:
+
+```json
+{
+ "mcpServers": {
+ "demo": {
+ "type": "streamable-http",
+ "url": "https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp",
+ "protocolEra": "modern"
+ }
+ }
+}
+```
+
+```bash
+npx @modelcontextprotocol/inspector --cli \
+ --config mcp.json --server demo --method tools/list
+```
+
+Without `--cli` the same command opens the Inspector's browser UI, where the opt-in is the connection's **Protocol Era** setting.
+
+`curl` works too — the protocol is JSON-RPC over POST. List the tools:
+
+```bash
+curl -s https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream' \
+ -H 'mcp-protocol-version: 2026-07-28' \
+ -H 'mcp-method: tools/list' \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
+```
+
+Call `add` — a tool call also carries the tool name in the `mcp-name` header:
+
+```bash
+curl -s https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream' \
+ -H 'mcp-protocol-version: 2026-07-28' \
+ -H 'mcp-method: tools/call' \
+ -H 'mcp-name: add' \
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":40},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
+```
+
+```json
+{
+ "result": {
+ "content": [{ "type": "text", "text": "42" }],
+ "structuredContent": { "sum": 42 }
+ },
+ "jsonrpc": "2.0",
+ "id": 2
+}
+```
+
+The response samples on this page are trimmed to the load-bearing keys — the live responses also carry protocol bookkeeping inside `result`: a `resultType` and a `_meta` block naming the server.
+
+Ask `slow_report` for progress — pass a `progressToken` and the response arrives as an event stream, one event per step, with the result last (`-N` keeps `curl` from buffering it):
+
+```bash
+curl -sN https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream' \
+ -H 'mcp-protocol-version: 2026-07-28' \
+ -H 'mcp-method: tools/call' \
+ -H 'mcp-name: slow_report' \
+ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"slow_report","arguments":{"steps":3},"_meta":{"progressToken":"p1","io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
+```
+
+```
+event: message
+data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"p1","progress":1,"total":3,"message":"step 1"}}
+
+event: message
+data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"p1","progress":2,"total":3,"message":"step 2"}}
+
+event: message
+data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"p1","progress":3,"total":3,"message":"step 3"}}
+
+event: message
+data: {"result":{"content":[{"type":"text","text":"completed 3 steps"}]},"jsonrpc":"2.0","id":3}
+```
+
+The same call without a `progressToken` returns one plain JSON response: both modes live on the one endpoint, and the server picks per request.
+
+## Claude Code
+
+The deployed endpoint is a complete server for any Streamable HTTP client, and Claude Code needs nothing but `--transport http` and the URL — no custom domain, no OAuth flags:
+
+```bash
+claude mcp add --transport http demo https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp
+claude mcp list
+```
+
+```
+demo: https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp (HTTP) - ✔ Connected
+```
+
+A headless call proves the round trip without opening the REPL — the phrasing of the reply is the model's and varies run to run; what it reliably carries is the tool's answer, 42:
+
+```bash
+claude -p "call the add tool from the demo MCP server with a=2 b=40" --allowedTools "mcp__demo__add"
+```
+
+```
+`add(a=2, b=40)` → `{"sum": 42}`
+```
+
+The custom domain enters the picture only when a server enforces OAuth and a human logs in through the client — that walkthrough is the [oauth-cognito README](../oauth-cognito/README.md#interactive-clients-need-a-custom-domain). `claude mcp remove demo` deregisters it.
+
+## Going further
+
+`serverless.yml` carries the next three steps as commented blocks, each with the reasoning next to it:
+
+- **Authentication** — enforcement is yours; the Framework never verifies tokens. `authorizer` puts your access control at API Gateway, in front of the function — one of your own authorizer functions, a Cognito user pool, or `aws_iam` — and `oauthDiscovery` publishes the protected-resource discovery document that tells clients where to log in. The [oauth-cognito](../oauth-cognito), [oauth-authorizer](../oauth-authorizer), and [oauth-in-module](../oauth-in-module) examples each deploy one enforcement shape end to end.
+- **Elicitation state** — `state: true` provisions a signing key in this stack and hands it to the server, so a tool can pause to ask the caller for input and trust what the retry brings back. The caller’s client has to speak the 2026-07-28 protocol revision for that flow — opt-in for clients built on the official SDK (`versionNegotiation: { mode: 'auto' }`); tools that never ask for input need nothing.
+- **A custom domain** — `provider.domain` fronts the whole API, MCP servers and `http` functions alike. Mapped at the root, it also puts the discovery document where MCP clients probe for it.
+
+The [MCP guide](https://www.serverless.com/framework/docs/providers/aws/guide/mcp) documents each of these in full, including how the `state` key reaches your module.
+
+## Relationship to the custom hosting examples
+
+The [mcp/custom](https://github.com/serverless/examples/tree/v4/mcp/custom) family hosts the same kind of server by hand, one directory per hosting approach — a written Lambda adapter, Hono, Express or Fastify behind Lambda Web Adapter, API Gateway REST or a Function URL — so you can see and compare the glue. This example is the shortcut: the Framework assembles the Lambda-plus-API-Gateway-streaming hosting for you, so reach for those when you want a different front door or control over the bridge, and for this when you want the endpoint without writing any of it.
+
+## Clean up
+
+```bash
+serverless remove
+```
diff --git a/mcp/minimal/package-lock.json b/mcp/minimal/package-lock.json
new file mode 100644
index 000000000..6845a1ed2
--- /dev/null
+++ b/mcp/minimal/package-lock.json
@@ -0,0 +1,50 @@
+{
+ "name": "mcp-minimal",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mcp-minimal",
+ "version": "1.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "zod": "^4.2.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/core": "2.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/mcp/minimal/package.json b/mcp/minimal/package.json
new file mode 100644
index 000000000..927b9192a
--- /dev/null
+++ b/mcp/minimal/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "mcp-minimal",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "MCP server deployed with the Serverless Framework",
+ "dependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "zod": "^4.2.0"
+ }
+}
diff --git a/mcp/minimal/serverless.yml b/mcp/minimal/serverless.yml
new file mode 100644
index 000000000..78c247d0b
--- /dev/null
+++ b/mcp/minimal/serverless.yml
@@ -0,0 +1,57 @@
+# An MCP server declared under `mcp:` below. The module in
+# src/server.mjs is a plain MCP SDK server; everything around it — the HTTPS
+# endpoint, response streaming, the Lambda entry that bridges the two, and the
+# packaging — belongs to the Framework.
+service: mcp-minimal
+frameworkVersion: '4'
+
+provider:
+ name: aws
+ runtime: nodejs24.x # the MCP SDK requires Node.js 20 or newer
+ # Edge-optimized REST endpoints (the default) end a response stream that has
+ # gone quiet for roughly 30 seconds, counted from the invoke; regional raises
+ # that bound to roughly 5 minutes. What matters is the gap between writes, so
+ # a tool that computes for a while before it answers needs the regional bound.
+ endpointType: REGIONAL
+
+ # A custom domain belongs to the API, not to one server: it fronts every MCP
+ # server and `http` function in the service. Mapped at the root (no
+ # `basePath`), the OAuth discovery document sits where MCP clients probe for
+ # it, which is what lets an interactive client discover your authorization
+ # server and log in itself.
+ # domain: mcp.example.com
+
+mcp:
+ servers:
+ # The server name is the function key, the Lambda name suffix, and the URL
+ # path segment: this one is served at /demo/mcp.
+ demo:
+ server: src/server.mjs
+
+ # Longer tools: `timeout` (seconds, default 60) sets the function timeout
+ # and the streaming integration timeout together, so the two cannot drift
+ # apart. A tool running past ~300 seconds must emit progress regardless.
+ # timeout: 300
+
+ # Access control is yours to enforce — the Framework never verifies
+ # tokens. `authorizer` puts your enforcement at API Gateway, in front of
+ # the function: one of your own authorizer functions by name, an
+ # http-event-style object (a Cognito user pool included), or aws_iam.
+ # `oauthDiscovery` is advertisement, not enforcement: it publishes the
+ # protected-resource discovery document at
+ # /.well-known/oauth-protected-resource/demo/mcp so clients can find
+ # where to log in. With neither, the endpoint is public. The
+ # oauth-cognito, oauth-authorizer, and oauth-in-module examples next
+ # door each carry one enforcement shape, deployable end to end.
+ # authorizer: verifyToken
+ # oauthDiscovery:
+ # issuer: https://example.us.auth0.com
+
+ # Elicitation round trips — a tool pausing to ask the caller for input —
+ # need a key to seal what the retry has to bring back. `state: true`
+ # provisions one in this stack (a Secrets Manager secret, about $0.40 per
+ # month) and places it in SERVERLESS_MCP_STATE_KEY before your module is
+ # imported, so `createRequestStateCodec` can read it at module level. A
+ # literal SSM parameter or Secrets Manager ARN brings your own key
+ # instead.
+ # state: true
diff --git a/mcp/minimal/src/server.mjs b/mcp/minimal/src/server.mjs
new file mode 100644
index 000000000..25fb76ff5
--- /dev/null
+++ b/mcp/minimal/src/server.mjs
@@ -0,0 +1,82 @@
+/**
+ * A standard MCP server, built with the official MCP TypeScript SDK. There is
+ * nothing Lambda-specific and nothing Serverless Framework-specific in here:
+ * the `mcp` property packages this module with an entry that hands its default
+ * export a web-standard request to answer.
+ *
+ * The default export must be what `createMcpHandler()` returns, and the module
+ * must be ESM.
+ */
+import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'
+import { z } from 'zod'
+
+export default createMcpHandler(() => {
+ const server = new McpServer({ name: 'demo', version: '1.0.0' })
+
+ // An instant tool. zod schemas sit next to the implementation, and returning
+ // structuredContent alongside the text lets clients consume a typed result.
+ server.registerTool(
+ 'add',
+ {
+ description: 'Add two numbers',
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
+ // .finite() with a message: an overflow to Infinity fails output
+ // validation either way, but zod's default text for it ("expected
+ // number, received number") is undiagnosable.
+ outputSchema: z.object({
+ sum: z.number().finite('a + b overflowed the double range'),
+ }),
+ },
+ async ({ a, b }) => {
+ const output = { sum: a + b }
+ return {
+ content: [{ type: 'text', text: String(output.sum) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ // A slow tool that reports progress. Each notification is a write on the
+ // response stream, so the client sees the work advancing instead of waiting —
+ // and the connection never goes quiet long enough to be cut. Emit progress
+ // from any tool that takes more than a moment; a tool running longer than
+ // ~300 seconds has to, whatever its `timeout` is.
+ server.registerTool(
+ 'slow_report',
+ {
+ description: 'Generate a report, reporting progress along the way',
+ // Bounded on purpose: nothing else stops steps: 9e15, and the only
+ // backstop past the schema is the function timeout.
+ inputSchema: z.object({ steps: z.number().int().min(1).max(60).default(5) }),
+ },
+ async ({ steps }, ctx) => {
+ // Clients that want progress send a token with the call; ones that do not
+ // get the same result without the notifications.
+ const progressToken = ctx.mcpReq._meta?.progressToken
+ for (let step = 1; step <= steps; step++) {
+ await new Promise((resolve) => setTimeout(resolve, 800))
+ if (progressToken !== undefined) {
+ await ctx.mcpReq.notify({
+ method: 'notifications/progress',
+ params: {
+ progressToken,
+ progress: step,
+ total: steps,
+ message: `step ${step}`,
+ },
+ })
+ }
+ }
+ return {
+ content: [
+ {
+ type: 'text',
+ text: `completed ${steps} ${steps === 1 ? 'step' : 'steps'}`,
+ },
+ ],
+ }
+ },
+ )
+
+ return server
+})
diff --git a/mcp/oauth-authorizer/README.md b/mcp/oauth-authorizer/README.md
new file mode 100644
index 000000000..5387d637d
--- /dev/null
+++ b/mcp/oauth-authorizer/README.md
@@ -0,0 +1,241 @@
+
+
+# MCP Server with a Custom Lambda Authorizer
+
+The bring-your-own-verification member of the [MCP examples](../README.md). The [Framework never verifies tokens](https://www.serverless.com/framework/docs/providers/aws/guide/mcp#authentication) — enforcement is yours — and when your identity provider is not Amazon Cognito (here it is **Auth0**), or the accept/reject decision needs logic of your own, the enforcement shape is a **Lambda authorizer**: one of your own functions, named on the server, wired by the Framework to the MCP route so API Gateway consults it before the server function is invoked. What it rejects, the server never runs for.
+
+```yaml
+functions:
+ mcpAuthorizer:
+ handler: src/authorizer.handler
+
+mcp:
+ servers:
+ demo:
+ server: src/server.mjs
+ authorizer: mcpAuthorizer
+ oauthDiscovery:
+ issuer: ${env:MCP_ISSUER}
+```
+
+The Framework wires the authorizer to the **MCP route only**. The discovery route deliberately stays open, because a client has to read that document before it has a token — an easy thing to get wrong when wiring this by hand. And `oauthDiscovery` is advertisement, not enforcement: the document tells clients where to log in; the authorizer is what rejects.
+
+## The shape of the event — read this before writing your own
+
+The string form above compiles a **TOKEN authorizer**, and a TOKEN authorizer receives exactly one thing: the `Authorization` header's value, as `event.authorizationToken`. There is no `event.headers`, no method, no path — an authorizer function written against the full request event finds nothing where it looks and rejects every call. `src/authorizer.mjs` is written against the TOKEN shape, and it is the right default: the event is minimal, and API Gateway rejects requests that omit the header without invoking the authorizer at all.
+
+When the decision genuinely needs more than the token — another header, the source IP, a query parameter — the object form's `type: request` is the escape hatch, delivering the full request event:
+
+```yaml
+mcp:
+ servers:
+ demo:
+ server: src/server.mjs
+ authorizer:
+ name: mcpAuthorizer
+ type: request
+ identitySource: method.request.header.Authorization
+```
+
+The object form accepts everything an `http` event's `authorizer` object does — `resultTtlInSeconds` to tune the verdict cache (300 seconds by default, per token; with this example's throw-style rejections only `Allow` verdicts ever enter it — the rejection table below covers why), `arn` for a function outside this service, `authorizerId` for an authorizer that already exists — and compiles through the same machinery, so the [API Gateway authorizer documentation](https://www.serverless.com/framework/docs/providers/aws/events/apigateway#http-endpoints-with-custom-authorizers) applies verbatim. The one exception is `claims`, which only does anything under API Gateway's `lambda` integration — an integration MCP routes never compile; leave it out.
+
+## What rejection looks like
+
+Every path out of the authorizer is API Gateway's own response — a bare `401 {"message":"Unauthorized"}` — never the MCP specification's challenge:
+
+| Request | Rejected by | Cost |
+| ---------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
+| No `Authorization` header | API Gateway, on the missing identity source | No invocation at all |
+| A garbage or expired token | The authorizer's JWKS check | One authorizer invocation — per request; rejections are never cached |
+| A valid token for another audience | The authorizer's audience rule | One authorizer invocation — per request; rejections are never cached |
+| A valid token | Nobody — the server runs | The server invocation you wanted; the `Allow` verdict is cached, so repeats inside the TTL skip the authorizer |
+
+The asymmetry in that table is deliberate. API Gateway caches only policy documents, and this authorizer rejects by throwing — a thrown rejection returns no policy, so nothing enters the cache and every rejected request re-invokes the function: three requests with the same bad token are three authorizer invocations, where three with the same valid token are one. Returning an explicit `Deny` policy instead would make rejections cache too — but API Gateway answers a `Deny` with `403` (its `ACCESS_DENIED` response), and it is the `401` that carries clients into the OAuth flow, so this example keeps the throw. Either way, what a rejection never costs is the server invocation — the function you actually pay to run. One operational consequence of the throw: each rejection lands in the authorizer's logs as an invocation error with a stack trace and counts toward its `Errors` metric. That is expected, not a malfunction — alarm on the server's errors instead, or log a structured line before throwing if you want a countable rejection signal.
+
+That bare `401` is still enough for the official SDK client, which starts its OAuth flow on any `401` and then probes the well-known discovery paths. Whether those probes then _find_ the document published by `oauthDiscovery` is a separate question, decided by the endpoint's shape: the probes look under the origin root, and on the raw `execute-api` URL this example deploys to the document sits under the stage prefix, where no probe lands — the root-mapped custom domain covered in the [oauth-cognito README](../oauth-cognito/README.md#interactive-clients-need-a-custom-domain) is what fixes that. Machine-to-machine callers like this example's, issued their tokens out of band, never probe at all. What no gateway rejection can carry is the spec's `WWW-Authenticate` challenge and scope-aware `403`s; when you want those semantics, or the caller's verified identity inside your tools, add the in-module gate from [oauth-in-module](../oauth-in-module) behind this authorizer — the two compose, cheap flood rejection in front and the spec's judgement behind it.
+
+## Setup
+
+Any OIDC issuer that mints machine-to-machine tokens works here: the authorizer's audience rule accepts a token's `aud` when present and its `client_id` otherwise, so both audience shapes in the wild pass unchanged. Auth0 is what the steps below use — and if you have no Auth0 tenant, an Amazon Cognito user pool stands in fine: the [oauth-cognito](../oauth-cognito) sibling declares a complete pool in its `resources:` and its README mints the tokens; point `MCP_ISSUER` at that pool's issuer URL and `MCP_AUDIENCE` at its app-client id.
+
+Install the [Auth0 CLI](https://github.com/auth0/auth0-cli) and log in to a tenant you own:
+
+```bash
+auth0 login
+```
+
+Create an API — its identifier is the audience your tokens will carry, and it does not have to resolve:
+
+```bash
+auth0 apis create --name "MCP Demo" --identifier "https://mcp.example.com" \
+ --scopes "invoke:tools" --token-lifetime 86400 --no-input
+```
+
+(The `invoke:tools` scope is declared but nothing in this example requests or checks it — the authorizer gates on issuer and audience only. Scope enforcement is the natural extension, and [oauth-in-module](../oauth-in-module) shows where it lives.)
+
+Create a machine-to-machine application for a service caller:
+
+```bash
+auth0 apps create --name "MCP Demo Client" --type m2m \
+ --description "Calls the MCP demo server" --no-input
+```
+
+The CLI prints the client id (the secret stays masked unless you pass `--reveal-secrets`, and nothing below needs it). Creating the application does not authorize it for the API — open it in the dashboard (`auth0 apps open `) and grant it access to the API you just created, or create the grant from the CLI through its Management API passthrough (`auth0 api post client-grants`).
+
+Point the example at that tenant. **The issuer must be exactly what your tenant's discovery document declares — including the trailing slash Auth0 uses** — because the JWKS check compares it verbatim against the token's `iss` claim:
+
+```bash
+export MCP_ISSUER="https://your-tenant.us.auth0.com/"
+export MCP_AUDIENCE="https://mcp.example.com"
+```
+
+The issuer-agnosticism promised at the top is the audience rule in `src/authorizer.mjs` at work: Auth0 access tokens carry the API identifier in `aud`, Amazon Cognito access tokens carry no `aud` at all and identify their caller in `client_id`, and the rule serves both. For a production deployment against Cognito, prefer [oauth-cognito](../oauth-cognito) — API Gateway validates pool tokens itself, no authorizer function needed; for trying this example's pattern out, the `client_id` fallback means a pool works here unchanged.
+
+## Deployment
+
+```bash
+npm install
+serverless deploy
+```
+
+```
+[!] MCP server "demo" advertises OAuth discovery at the stage URL, which interactive clients cannot discover. …
+mcp: demo → https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp
+```
+
+The warning is expected on this example's default endpoint: the machine-to-machine callers it centers on never probe for the discovery document, and the [rejection section](#what-rejection-looks-like) covers the endpoint shape. When interactive clients enter the picture, a root-mapped custom domain resolves it — declare it under `provider.domain`, or point `mcp.servers.demo.oauthDiscovery.publicUrl` at a domain managed outside this service.
+
+## Testing
+
+Mint a token for the machine client. The Auth0 CLI does it without you handling the secret:
+
+```bash
+auth0 test token --audience "https://mcp.example.com" --force
+```
+
+Set `ENDPOINT` to the deployed URL and `TOKEN` to that access token.
+
+**A request with no token — rejected on the missing identity source, before any invocation:**
+
+```bash
+curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$ENDPOINT" \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream' \
+ -H 'mcp-method: tools/list' \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
+```
+
+```
+401
+```
+
+**A garbage token — rejected by the authorizer's verification.** The proof that the server never ran is the absence of a log line: the rejection appears in `serverless logs -f mcpAuthorizer`, and `serverless logs -f demo` has nothing for either request (before any authorized call it errors with `No existing streams for the function` — the deploy created the server's log group, but nothing has invoked the function to write a stream into it):
+
+```bash
+curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$ENDPOINT" \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream' \
+ -H 'mcp-method: tools/list' \
+ -H 'authorization: Bearer not-a-token' \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
+```
+
+```
+401
+```
+
+**The discovery document answers without a token**, because the authorizer is not on that route:
+
+```bash
+curl -sS "https://abc123def.execute-api.us-east-1.amazonaws.com/dev/.well-known/oauth-protected-resource/demo/mcp"
+```
+
+```json
+{
+ "resource": "https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp",
+ "authorization_servers": ["https://your-tenant.us.auth0.com/"],
+ "bearer_methods_supported": ["header"]
+}
+```
+
+**A valid token passes the authorizer and reaches the server:**
+
+```bash
+curl -sS -X POST "$ENDPOINT" \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream' \
+ -H 'mcp-method: tools/call' -H 'mcp-name: add' \
+ -H "authorization: Bearer $TOKEN" \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":40},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
+```
+
+```json
+{
+ "result": {
+ "content": [{ "type": "text", "text": "42" }],
+ "structuredContent": { "sum": 42 },
+ "resultType": "complete"
+ },
+ "jsonrpc": "2.0",
+ "id": 1
+}
+```
+
+(Trimmed — the live response also carries a `_meta` block naming the server.)
+
+The [hub README](../README.md) has the official-client script (pass the token as `BEARER`) and the capability matrix that applies to every example in the family.
+
+## Claude Code: URL-only (Dynamic Client Registration)
+
+There are two ways an interactive client ends up with a client id for your issuer:
+
+- **Pre-registration** — you create the app client and hand its id to the client. This works against every issuer, including those without registration support; the [oauth-cognito README](../oauth-cognito/README.md#interactive-clients-need-a-custom-domain) walks that path (`claude mcp add … --client-id --callback-port `).
+- **Dynamic Client Registration (DCR)** — the client registers _itself_ with the issuer, and needs nothing but the URL. The URL is the server's real address — the root-mapped custom domain this path requires (covered below), not the Setup section's API identifier, which never resolves:
+
+ ```bash
+ claude mcp add --transport http demo https://mcp.your-domain.com/demo/mcp
+ ```
+
+ On the first authenticate, the client reads the document `oauthDiscovery` publishes, finds the `registration_endpoint` in the issuer's metadata, registers, and runs the same authorization-code + PKCE login a pre-registered client runs — choosing its own local callback port along the way, so not even `--callback-port` is needed.
+
+Nothing on the server side is different between the two: registration is a client-to-issuer affair, and this example's authorizer verifies the resulting token exactly as it verifies any other — issuer, signature, expiry, audience. The audience is the leg to align deliberately, because a URL-only client identifies the server by URL alone: it sends the MCP endpoint's URL as `resource` (RFC 8707) on its authorization and token requests, never an Auth0-style `audience` parameter. Meeting it needs two things: the API registered at the issuer under **the MCP endpoint's URL as its identifier** (not the bare origin used in the M2M setup above — following this path means changing that identifier and `MCP_AUDIENCE` to match), and the issuer actually honoring the `resource` parameter — on Auth0 that is the tenant's **Resource Parameter Compatibility Profile**, disabled by default. And as with every interactive flow in this family, the discovery leg needs a root-mapped custom domain — the [rejection section](#what-rejection-looks-like) covers why.
+
+Beyond that, what the URL-only path requires is on the issuer's side. For Auth0, at least: dynamic client registration enabled on the tenant, a connection available to dynamically-registered clients (they register as **third-party** applications, which can only use domain-level connections), and the **New Universal Login** experience — Auth0 does not serve third-party clients from the Classic one. Legacy Rules are their own hurdle: a strict-mode third-party client is refused outright when any Rule is enabled, with an error that surfaces only after the login itself — migrating Rules to Actions clears it, or the tenant's registration security mode can place registered clients in permissive mode, where Rules still run. Expect a consent screen — third-party applications always ask. Two of these switches change the whole tenant's posture, so treat them deliberately: while registration is enabled, the registration endpoint is unauthenticated by design (anyone who knows the tenant domain can register a client), and a domain-level connection is available to every third-party application, not just this one — prefer a dedicated connection you can delete afterwards over promoting a shared one.
+
+One diagnostic to know: when this path fails, clients tend to say very little — Claude Code reports a bare `SDK auth failed:` with no detail at all. The issuer's real answer is one request away. With registration disabled, for example:
+
+```bash
+curl -s -X POST https://your-tenant.us.auth0.com/oidc/register \
+ -H 'content-type: application/json' \
+ -d '{"client_name":"probe","redirect_uris":["http://localhost:8976/callback"]}'
+```
+
+```json
+{
+ "statusCode": 400,
+ "error": "Bad Request",
+ "message": "dynamic client registration is disabled"
+}
+```
+
+The issuer's metadata advertises a `registration_endpoint` whether or not registration is actually enabled, so the probe — not the metadata — is what tells you.
+
+## Clean up
+
+```bash
+serverless remove
+auth0 apis delete "https://mcp.example.com" --force
+auth0 apps delete --force
+```
+
+If you enabled the URL-only path, also revert the tenant switches: disable dynamic client registration, demote (or delete) the connection you made domain-level, and delete any clients that registered themselves while it was on — they are ordinary applications in the dashboard, named by the client that created them. And if you re-registered the API under the MCP endpoint's URL, delete it by that identifier rather than the one above.
diff --git a/mcp/oauth-authorizer/package-lock.json b/mcp/oauth-authorizer/package-lock.json
new file mode 100644
index 000000000..502259838
--- /dev/null
+++ b/mcp/oauth-authorizer/package-lock.json
@@ -0,0 +1,60 @@
+{
+ "name": "mcp-oauth-authorizer",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mcp-oauth-authorizer",
+ "version": "1.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "jose": "^6.1.0",
+ "zod": "^4.2.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/core": "2.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/jose": {
+ "version": "6.2.6",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.6.tgz",
+ "integrity": "sha512-HwMtbJjMw8rC8dUTwCNilHJD+fxTeKM3JV1eprSmTjS41qwXSSt6exJXgyPK1QOu0jB9eDYLESRDkB3qaT3jnw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/mcp/oauth-authorizer/package.json b/mcp/oauth-authorizer/package.json
new file mode 100644
index 000000000..91f245a46
--- /dev/null
+++ b/mcp/oauth-authorizer/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "mcp-oauth-authorizer",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "MCP server deployed with the Serverless Framework",
+ "dependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "jose": "^6.1.0",
+ "zod": "^4.2.0"
+ }
+}
diff --git a/mcp/oauth-authorizer/serverless.yml b/mcp/oauth-authorizer/serverless.yml
new file mode 100644
index 000000000..e8329a5e0
--- /dev/null
+++ b/mcp/oauth-authorizer/serverless.yml
@@ -0,0 +1,47 @@
+# An MCP server whose route is fronted by your own Lambda authorizer, so
+# unauthorized traffic is rejected by API Gateway before the server function is
+# invoked. The identity provider here is Auth0; the authorizer's JWKS check
+# fits any OIDC issuer.
+#
+# The Framework never verifies tokens — enforcement is this authorizer, and
+# every request it rejects is a server invocation you do not pay for.
+service: mcp-oauth-authorizer
+frameworkVersion: '4'
+
+provider:
+ name: aws
+ runtime: nodejs24.x # the MCP SDK requires Node.js 20 or newer
+ # Lets a response stream stay quiet for roughly 5 minutes; the edge-optimized
+ # default ends one quiet for roughly 30 seconds.
+ endpointType: REGIONAL
+
+functions:
+ # An ordinary function of yours. `authorizer:` below names it by key, and the
+ # Framework wires it to the MCP route only — never to the discovery route,
+ # which a client must be able to read before it has a token to present.
+ mcpAuthorizer:
+ handler: src/authorizer.handler
+ environment:
+ MCP_ISSUER: ${env:MCP_ISSUER}
+ MCP_AUDIENCE: ${env:MCP_AUDIENCE}
+
+mcp:
+ servers:
+ demo:
+ server: src/server.mjs
+
+ # The string form: a bare function name compiles as a TOKEN authorizer,
+ # with API Gateway's defaults — an Allow verdict cached per token for 300
+ # seconds (rejections throw, return no policy, and are never cached), and
+ # a request with no Authorization header at all rejected without invoking
+ # even the authorizer. A TOKEN authorizer receives ONLY
+ # the header's value, as event.authorizationToken; src/authorizer.mjs is
+ # written against exactly that shape, and the README covers the
+ # `type: request` object form for decisions that need the full request.
+ authorizer: mcpAuthorizer
+
+ # Advertisement, not enforcement: publishes the protected-resource
+ # discovery document (RFC 9728) naming the issuer, so clients can find
+ # where to log in. What rejects is the authorizer above.
+ oauthDiscovery:
+ issuer: ${env:MCP_ISSUER}
diff --git a/mcp/oauth-authorizer/src/authorizer.mjs b/mcp/oauth-authorizer/src/authorizer.mjs
new file mode 100644
index 000000000..233e5b411
--- /dev/null
+++ b/mcp/oauth-authorizer/src/authorizer.mjs
@@ -0,0 +1,88 @@
+/**
+ * A Lambda authorizer for the MCP route: API Gateway calls this before the
+ * server function, so a request without a valid token never reaches it — and
+ * never bills it. The Framework wires it up from `authorizer: mcpAuthorizer`
+ * in serverless.yml; the Framework itself verifies nothing, so what this
+ * function accepts IS the server's access control.
+ *
+ * Verification is a standard JWKS check, so `jose` is a dependency of this
+ * example. The remote key set is created once per execution environment and
+ * caches keys across invocations, and API Gateway caches the Allow policy this
+ * function returns, per token, for the TTL configured on the authorizer (300
+ * seconds by default). Rejections throw instead of returning a policy, so they
+ * are never cached — every rejected request re-invokes this function.
+ */
+import { createRemoteJWKSet, jwtVerify } from 'jose'
+
+const ISSUER = process.env.MCP_ISSUER
+const AUDIENCE = process.env.MCP_AUDIENCE
+
+// Module scope on purpose: one key set per execution environment, reused by
+// every invocation it serves.
+const jwks = createRemoteJWKSet(
+ new URL('.well-known/jwks.json', ISSUER.endsWith('/') ? ISSUER : `${ISSUER}/`),
+)
+
+export const handler = async (event) => {
+ // Naming an authorizer as a bare string gives you a TOKEN authorizer, and a
+ // TOKEN authorizer receives ONLY the Authorization header's value, as
+ // `event.authorizationToken` — there is no `event.headers`, so an authorizer
+ // written against the full request event finds nothing where it looks and
+ // rejects every call. (The object form with `type: request` is the one that
+ // gets headers; it costs a wider event and is only needed when the decision
+ // depends on more than the token.)
+ const header = event.authorizationToken
+ const token = header?.replace(/^Bearer /i, '')
+
+ // "Unauthorized" (the literal string) is what API Gateway turns into a 401.
+ // Anything else it reports as a 500, so a verification failure has to look
+ // like this rather than like an error.
+ if (!token || token === header) throw new Error('Unauthorized')
+
+ try {
+ const { payload } = await jwtVerify(token, jwks, {
+ issuer: ISSUER,
+ requiredClaims: ['exp'],
+ })
+
+ // The audience, checked by hand rather than passed to jwtVerify: an Auth0
+ // access token carries the API identifier in `aud`, while a Cognito access
+ // token has no `aud` at all and identifies its caller in `client_id` — so
+ // the rule is "match `aud` when present, `client_id` otherwise", and one
+ // authorizer serves both issuer families.
+ const presented = payload.aud ?? payload.client_id
+ const values = Array.isArray(presented) ? presented : [presented]
+ if (!values.includes(AUDIENCE)) throw new Error('audience mismatch')
+
+ return {
+ // The gateway caches by token, so the principal must be derived from the
+ // token rather than fixed — a shared principal would blur its cache
+ // entries together.
+ principalId: payload.sub ?? 'unknown',
+ policyDocument: {
+ Version: '2012-10-17',
+ Statement: [
+ {
+ Action: 'execute-api:Invoke',
+ Effect: 'Allow',
+ // The whole stage, not `event.methodArn`: the gateway caches this
+ // document per token, and a per-request resource would authorize
+ // only the first request the cache was filled from.
+ Resource: `${event.methodArn.split('/').slice(0, 2).join('/')}/*`,
+ },
+ ],
+ },
+ // Context is delivered into the server function's EVENT, but the MCP
+ // entry hands your module only the HTTP request itself — identity your
+ // tools consume has to come from an in-module gate instead (see the
+ // oauth-in-module example). Populated here anyway: it lands in API
+ // Gateway's access logs, where it is genuinely useful. Values must be
+ // strings, numbers or booleans — API Gateway drops objects.
+ context: { sub: String(payload.sub ?? ''), scope: String(payload.scope ?? '') },
+ }
+ } catch {
+ // Deliberately opaque: the reason a token failed is for your logs, not for
+ // the caller.
+ throw new Error('Unauthorized')
+ }
+}
diff --git a/mcp/oauth-authorizer/src/server.mjs b/mcp/oauth-authorizer/src/server.mjs
new file mode 100644
index 000000000..25fb76ff5
--- /dev/null
+++ b/mcp/oauth-authorizer/src/server.mjs
@@ -0,0 +1,82 @@
+/**
+ * A standard MCP server, built with the official MCP TypeScript SDK. There is
+ * nothing Lambda-specific and nothing Serverless Framework-specific in here:
+ * the `mcp` property packages this module with an entry that hands its default
+ * export a web-standard request to answer.
+ *
+ * The default export must be what `createMcpHandler()` returns, and the module
+ * must be ESM.
+ */
+import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'
+import { z } from 'zod'
+
+export default createMcpHandler(() => {
+ const server = new McpServer({ name: 'demo', version: '1.0.0' })
+
+ // An instant tool. zod schemas sit next to the implementation, and returning
+ // structuredContent alongside the text lets clients consume a typed result.
+ server.registerTool(
+ 'add',
+ {
+ description: 'Add two numbers',
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
+ // .finite() with a message: an overflow to Infinity fails output
+ // validation either way, but zod's default text for it ("expected
+ // number, received number") is undiagnosable.
+ outputSchema: z.object({
+ sum: z.number().finite('a + b overflowed the double range'),
+ }),
+ },
+ async ({ a, b }) => {
+ const output = { sum: a + b }
+ return {
+ content: [{ type: 'text', text: String(output.sum) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ // A slow tool that reports progress. Each notification is a write on the
+ // response stream, so the client sees the work advancing instead of waiting —
+ // and the connection never goes quiet long enough to be cut. Emit progress
+ // from any tool that takes more than a moment; a tool running longer than
+ // ~300 seconds has to, whatever its `timeout` is.
+ server.registerTool(
+ 'slow_report',
+ {
+ description: 'Generate a report, reporting progress along the way',
+ // Bounded on purpose: nothing else stops steps: 9e15, and the only
+ // backstop past the schema is the function timeout.
+ inputSchema: z.object({ steps: z.number().int().min(1).max(60).default(5) }),
+ },
+ async ({ steps }, ctx) => {
+ // Clients that want progress send a token with the call; ones that do not
+ // get the same result without the notifications.
+ const progressToken = ctx.mcpReq._meta?.progressToken
+ for (let step = 1; step <= steps; step++) {
+ await new Promise((resolve) => setTimeout(resolve, 800))
+ if (progressToken !== undefined) {
+ await ctx.mcpReq.notify({
+ method: 'notifications/progress',
+ params: {
+ progressToken,
+ progress: step,
+ total: steps,
+ message: `step ${step}`,
+ },
+ })
+ }
+ }
+ return {
+ content: [
+ {
+ type: 'text',
+ text: `completed ${steps} ${steps === 1 ? 'step' : 'steps'}`,
+ },
+ ],
+ }
+ },
+ )
+
+ return server
+})
diff --git a/mcp/oauth-cognito/README.md b/mcp/oauth-cognito/README.md
new file mode 100644
index 000000000..d9bac1788
--- /dev/null
+++ b/mcp/oauth-cognito/README.md
@@ -0,0 +1,241 @@
+
+
+# MCP Server with Amazon Cognito OAuth
+
+The zero-code OAuth member of the [MCP examples](../README.md). The [Framework never verifies tokens](https://www.serverless.com/framework/docs/providers/aws/guide/mcp#authentication) — enforcement is yours — and this example puts all of it where it costs the least: an **Amazon Cognito user pool authorizer**, validated by API Gateway itself. A rejected request invokes nothing: not the server function, and no authorizer function either, because there isn't one. The server module stays a plain SDK server; every AWS resource involved — the pool, its OAuth scope, a machine-to-machine app client — is declared in this example's `resources:`, so one deploy stands up the whole story and one `remove` tears it down.
+
+```yaml
+mcp:
+ servers:
+ demo:
+ server: src/server.mjs
+ authorizer:
+ name: demoPool
+ type: COGNITO_USER_POOLS
+ arn: !GetAtt UserPool.Arn
+ scopes:
+ - mcp/invoke
+ oauthDiscovery:
+ issuer: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}"
+```
+
+Three things in that block are load-bearing:
+
+- **`name` and `type` are written out** because the pool lives in this stack: with `arn` as a `Fn::GetAtt` there is no literal ARN to derive them from. (A pool that already exists is referenced by its literal ARN instead, and still needs the `name` — the one API Gateway would derive from a pool ARN is not a valid CloudFormation identifier.)
+- **`scopes` selects access tokens.** With scopes configured, API Gateway validates the caller's Cognito **access token** against them; with no scopes it validates identity tokens instead — and a `client_credentials` client mints only access tokens, so without the scope nothing this example creates could call it.
+- **`oauthDiscovery` is advertisement, not enforcement.** It publishes the RFC 9728 protected-resource document so clients can find the authorization server; the authorizer above is what actually rejects. The document is rendered at deploy time, and the pool id inside the issuer exists only once the stack is created — which is why the issuer is a [CloudFormation intrinsic](https://www.serverless.com/framework/docs/providers/aws/guide/mcp#an-issuer-created-by-the-same-stack) rather than a literal URL: the `!Sub` is resolved by the same deploy that creates the pool, so the published document names the real issuer from the start.
+
+## What the gate accepts
+
+Acceptance is scoped to the **pool and the scope, never to a client**: a token from _any_ app client of the same pool carrying `mcp/invoke` is accepted, and API Gateway does not narrow further. Per-client access control therefore means distinct scopes — or distinct pools. Rejections are API Gateway's own bare responses, and they are all the same one: a `401` (`{"message":"Unauthorized"}`) whether the token is missing, unverifiable, or verified but lacking the scope — never the MCP specification's challenge, and never a status code that says which failure it was, so a scope problem looks exactly like no token at all. The bare `401` is still enough to start the official SDK client's OAuth flow. When you need the spec's semantics — per-scope `403`s, `WWW-Authenticate` challenges, the caller's identity inside your tools — that is the [oauth-in-module](../oauth-in-module) example, and the two compose.
+
+## Deployment
+
+```bash
+npm install
+serverless deploy
+```
+
+One deploy stands up everything — the pool, its scope, the app client, the server, and the discovery document, which names the real pool from the start: the `!Sub` in the issuer is resolved by the same deploy that creates the pool. If you fetch the document right after a deploy that changed it, give it a minute or two — API Gateway can briefly keep serving the previous body after the deploy returns, and it converges on its own.
+
+The deploy also prints a warning that the discovery document lives at the stage URL, where interactive clients cannot discover it. That is expected on this example's default endpoint: the [Interactive clients](#interactive-clients-need-a-custom-domain) section below is the fix, and the machine-to-machine caller this example centers on never reads the document at all.
+
+```
+mcp: demo → https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp
+```
+
+Set `ENDPOINT` to that URL for everything below.
+
+## Testing
+
+### The negatives first — no code of yours runs for these
+
+No token is API Gateway's own bare `401`. No MCP challenge, and no invocation — `serverless logs -f demo` has nothing to show and says so with an error, `No existing streams for the function`: nothing has ever invoked the function, so the log group the deploy created holds no streams yet:
+
+```bash
+curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$ENDPOINT" \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream' \
+ -H 'mcp-method: tools/list' \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
+```
+
+```
+401
+```
+
+A garbage token gets the same `401` — API Gateway judged it against the pool's keys and rejected it, still without invoking anything:
+
+```bash
+curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$ENDPOINT" \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream' \
+ -H 'mcp-method: tools/list' \
+ -H 'authorization: Bearer not-a-token' \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
+```
+
+```
+401
+```
+
+The discovery document reads without a token — the authorizer is deliberately never wired to that route, because a client has to learn where to log in before it has anything to present. Its well-known path sits between the stage root and the server path, so derive it from `ENDPOINT` rather than typing it:
+
+```bash
+DISCOVERY="${ENDPOINT%/demo/mcp}/.well-known/oauth-protected-resource/demo/mcp"
+curl -sS "$DISCOVERY"
+```
+
+```json
+{
+ "resource": "https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp",
+ "authorization_servers": [
+ "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX"
+ ],
+ "bearer_methods_supported": ["header"]
+}
+```
+
+### Mint a token and call a tool
+
+Everything a caller needs is in the stack's outputs, except the client secret, which Cognito holds — read it with `describe-user-pool-client`:
+
+```bash
+STACK=mcp-oauth-cognito-dev # -: the -dev suffix tracks the stage you deployed to
+out() { aws cloudformation describe-stacks --stack-name "$STACK" \
+ --query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" --output text; }
+
+POOL_ID=$(out UserPoolId)
+CLIENT_ID=$(out ClientId)
+TOKEN_ENDPOINT=$(out TokenEndpoint)
+CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client \
+ --user-pool-id "$POOL_ID" --client-id "$CLIENT_ID" \
+ --query 'UserPoolClient.ClientSecret' --output text)
+
+TOKEN=$(curl -sS -X POST "$TOKEN_ENDPOINT" \
+ -H 'content-type: application/x-www-form-urlencoded' \
+ -u "$CLIENT_ID:$CLIENT_SECRET" \
+ -d 'grant_type=client_credentials&scope=mcp/invoke' \
+ | node -e 'process.stdin.on("data",d=>console.log(JSON.parse(d).access_token))')
+```
+
+```bash
+curl -sS -X POST "$ENDPOINT" \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream' \
+ -H 'mcp-method: tools/call' -H 'mcp-name: add' \
+ -H "authorization: Bearer $TOKEN" \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":40},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
+```
+
+```json
+{
+ "result": {
+ "content": [{ "type": "text", "text": "42" }],
+ "structuredContent": { "sum": 42 },
+ "resultType": "complete"
+ },
+ "jsonrpc": "2.0",
+ "id": 1
+}
+```
+
+(Trimmed — the live response also carries a `_meta` block naming the server.)
+
+The [hub README](../README.md) has the official-client script (pass the token as `BEARER`) and the capability matrix that applies to every example in the family.
+
+## Claude Code
+
+The same minted token also puts **Claude Code** on this endpoint — no custom domain, no login flow, because the header carries what the login flow would have obtained. The `--` is required after `--header`, which takes multiple values and would otherwise swallow the name and URL that follow it; commands without `--header` need no separator:
+
+```bash
+claude mcp add --transport http --header "Authorization: Bearer $TOKEN" -- demo "$ENDPOINT"
+claude mcp list
+```
+
+```
+demo: https://abc123def.execute-api.us-east-1.amazonaws.com/dev/demo/mcp (HTTP) - ✔ Connected
+```
+
+```bash
+claude -p "call the add tool from the demo MCP server with a=2 b=40" --allowedTools "mcp__demo__add"
+```
+
+```
+`add(a=2, b=40)` → `{"sum": 42}`
+```
+
+The phrasing of that reply is the model's and varies run to run; what it reliably carries is the tool's answer, obtained through the full authenticated path. The header is pasted once and never refreshed, so calls start answering `401` when the token expires (an hour, on this pool's defaults) — re-add with a fresh token, or use the interactive flow below, which obtains and refreshes tokens itself. `claude mcp remove demo` deregisters it.
+
+## Interactive clients need a custom domain
+
+Everything above is machine-to-machine: the caller mints its own token and sends it. An **interactive** client — Claude Code, an IDE assistant — instead discovers your authorization server from the endpoint and runs a browser login, and on the raw `execute-api` URL that discovery cannot work: clients probe `/.well-known/oauth-protected-resource` and its siblings relative to the **origin root**, and on `execute-api` the document sits under the stage prefix (`/dev/.well-known/…`) where no conventional probe looks. A client whose probes all miss can quietly fall back to treating the endpoint's own origin as the authorization server and open `/authorize` — a URL nothing serves — so the symptom is a sign-in page that never loads, not an error.
+
+A [custom domain](https://www.serverless.com/framework/docs/providers/aws/guide/domains) mapped at the root fixes it — the stage prefix disappears, and the document sits exactly where every probe looks:
+
+```yaml
+provider:
+ domain: mcp.example.com # root-mapped: no basePath
+```
+
+Two Cognito-specific things to arrange alongside the domain:
+
+- **Cognito has no Dynamic Client Registration**, so an interactive client cannot register itself — pre-register an authorization-code + PKCE app client (no secret, a fixed `http://localhost:/callback` redirect) and hand its id to the client. A user to log in as completes the picture:
+
+ ```bash
+ CALLBACK_PORT=8976
+ INTERACTIVE_CLIENT_ID=$(aws cognito-idp create-user-pool-client \
+ --user-pool-id "$POOL_ID" --client-name interactive --no-generate-secret \
+ --allowed-o-auth-flows code \
+ --allowed-o-auth-scopes openid mcp/invoke \
+ --allowed-o-auth-flows-user-pool-client \
+ --supported-identity-providers COGNITO \
+ --callback-urls "http://localhost:$CALLBACK_PORT/callback" \
+ --query 'UserPoolClient.ClientId' --output text)
+
+ aws cognito-idp admin-create-user --user-pool-id "$POOL_ID" \
+ --username mcp-tester --message-action SUPPRESS \
+ --user-attributes Name=email,Value=mcp-tester@example.com Name=email_verified,Value=true
+ aws cognito-idp admin-set-user-password --user-pool-id "$POOL_ID" \
+ --username mcp-tester --password 'ChangeMe!2026' --permanent
+ ```
+
+- Redeploy after setting the domain, so the discovery document advertises it — and point every client at the domain URL from then on: a spec-conformant client that reaches the server on the old `execute-api` URL sees a `resource` naming a different origin than the one it called, and refuses the server.
+
+With both in place, Claude Code runs the whole flow itself:
+
+```bash
+claude mcp add --transport http --client-id "$INTERACTIVE_CLIENT_ID" \
+ --callback-port 8976 demo https://mcp.example.com/demo/mcp
+claude
+```
+
+The two extra flags are what stand in for Dynamic Client Registration: because Cognito has no DCR, the client cannot register itself, so you hand it the pre-registered client id — and since a pre-registered client carries a fixed redirect URI, the callback port has to match it too. Against an issuer that does support registration, the same command needs only the URL; the [oauth-authorizer README](../oauth-authorizer/README.md#claude-code-url-only-dynamic-client-registration) covers that path.
+
+Ask it to use the server; it discovers the issuer, finds it needs authorization, and hands you the hosted-UI URL to sign in on:
+
+
+
+The redirect completes the flow back to Claude:
+
+
+
+The logged-in calls then pass the gate. What makes them pass is scope: the pool authorizer accepts only access tokens carrying `mcp/invoke`, which is why the pre-registered client above enables `openid mcp/invoke` — and when a client's request names no scopes, Cognito issues the token with every scope enabled on the app client. If a post-login call answers `401` anyway, what the client sent to `/oauth2/authorize` is the first thing to check.
+
+## Clean up
+
+```bash
+serverless remove
+```
+
+The pool, its domain, and the app client are stack resources, so they leave with the stack. If you registered the server with Claude Code, also `claude mcp remove demo`.
diff --git a/mcp/oauth-cognito/images/claude-code-auth-success.png b/mcp/oauth-cognito/images/claude-code-auth-success.png
new file mode 100644
index 000000000..6ad1a7ee2
Binary files /dev/null and b/mcp/oauth-cognito/images/claude-code-auth-success.png differ
diff --git a/mcp/oauth-cognito/images/cognito-hosted-ui-sign-in.png b/mcp/oauth-cognito/images/cognito-hosted-ui-sign-in.png
new file mode 100644
index 000000000..50e2060ae
Binary files /dev/null and b/mcp/oauth-cognito/images/cognito-hosted-ui-sign-in.png differ
diff --git a/mcp/oauth-cognito/package-lock.json b/mcp/oauth-cognito/package-lock.json
new file mode 100644
index 000000000..b38bc70f1
--- /dev/null
+++ b/mcp/oauth-cognito/package-lock.json
@@ -0,0 +1,50 @@
+{
+ "name": "mcp-oauth-cognito",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mcp-oauth-cognito",
+ "version": "1.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "zod": "^4.2.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/core": "2.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/mcp/oauth-cognito/package.json b/mcp/oauth-cognito/package.json
new file mode 100644
index 000000000..10d677032
--- /dev/null
+++ b/mcp/oauth-cognito/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "mcp-oauth-cognito",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "MCP server deployed with the Serverless Framework",
+ "dependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "zod": "^4.2.0"
+ }
+}
diff --git a/mcp/oauth-cognito/serverless.yml b/mcp/oauth-cognito/serverless.yml
new file mode 100644
index 000000000..08c841c35
--- /dev/null
+++ b/mcp/oauth-cognito/serverless.yml
@@ -0,0 +1,107 @@
+# An MCP server protected by an Amazon Cognito user pool that API Gateway
+# validates itself — the zero-code OAuth path. There is no token-verification
+# code anywhere in this example: the pool, its OAuth scope, and a
+# machine-to-machine app client are all declared under `resources:` below, the
+# `authorizer` hands the pool to API Gateway, and a rejected request costs no
+# invocation anywhere — not the server function, not an authorizer function.
+service: mcp-oauth-cognito
+frameworkVersion: '4'
+
+provider:
+ name: aws
+ runtime: nodejs24.x # the MCP SDK requires Node.js 20 or newer
+ # Lets a response stream stay quiet for roughly 5 minutes; the edge-optimized
+ # default ends one quiet for roughly 30 seconds.
+ endpointType: REGIONAL
+
+mcp:
+ servers:
+ demo:
+ server: src/server.mjs
+
+ # A Cognito user pool authorizer needs no authorizer function of yours:
+ # API Gateway validates the JWT against the pool itself. The pool lives
+ # in this stack, so `arn` is a GetAtt — which is why `name` and `type`
+ # are written out: neither can be derived from an ARN that does not
+ # exist yet.
+ authorizer:
+ name: demoPool
+ type: COGNITO_USER_POOLS
+ arn: !GetAtt UserPool.Arn
+ # Scopes are what make API Gateway validate ACCESS tokens: with no
+ # scopes configured it validates identity tokens instead, and a
+ # client_credentials client mints only access tokens. Acceptance is
+ # scoped to the pool and the scope, never to a client — the README's
+ # "what the gate accepts" section spells out what that means.
+ scopes:
+ - mcp/invoke
+
+ # Advertisement, so clients can find the authorization server: the
+ # protected-resource discovery document (RFC 9728), served by API Gateway
+ # at /.well-known/oauth-protected-resource/demo/mcp with no Lambda behind
+ # it. The document is rendered at deploy time, and the pool id inside the
+ # issuer exists only once the stack is created — so the issuer is a
+ # CloudFormation intrinsic, resolved by the same deploy that creates the
+ # pool: one deploy publishes the real issuer.
+ oauthDiscovery:
+ issuer: !Sub 'https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}'
+
+resources:
+ Resources:
+ UserPool:
+ Type: AWS::Cognito::UserPool
+ Properties:
+ UserPoolName: ${self:service}-${sls:stage}
+ # The cheapest tier; machine-to-machine tokens need no customization.
+ UserPoolTier: LITE
+
+ # The scope tokens will carry: resource-server identifier "mcp", scope
+ # "invoke" — the full scope string a token presents is "mcp/invoke".
+ UserPoolResourceServer:
+ Type: AWS::Cognito::UserPoolResourceServer
+ Properties:
+ UserPoolId: !Ref UserPool
+ Identifier: mcp
+ Name: mcp
+ Scopes:
+ - ScopeName: invoke
+ ScopeDescription: Invoke the MCP server's tools
+
+ # The hosted domain serves the /oauth2/token endpoint tokens are minted
+ # against. The prefix must be globally unique — and must not contain the
+ # strings "aws", "amazon", or "cognito".
+ UserPoolDomain:
+ Type: AWS::Cognito::UserPoolDomain
+ Properties:
+ UserPoolId: !Ref UserPool
+ Domain: !Sub 'mcp-oauth-example-${AWS::AccountId}-${sls:stage}'
+
+ # The machine-to-machine caller: a confidential client that mints its own
+ # tokens with client_credentials and the scope above. The secret is
+ # generated by Cognito and never appears in this template or the stack's
+ # outputs — the README reads it with `describe-user-pool-client`.
+ UserPoolClient:
+ Type: AWS::Cognito::UserPoolClient
+ # The scope must exist before a client may list it.
+ DependsOn: UserPoolResourceServer
+ Properties:
+ UserPoolId: !Ref UserPool
+ ClientName: m2m
+ GenerateSecret: true
+ AllowedOAuthFlows:
+ - client_credentials
+ AllowedOAuthScopes:
+ - mcp/invoke
+ AllowedOAuthFlowsUserPoolClient: true
+
+ Outputs:
+ # Read by the README's token walkthrough (and by the oauth-in-module
+ # sibling, when this pool serves as its issuer).
+ UserPoolId:
+ Value: !Ref UserPool
+ ClientId:
+ Value: !Ref UserPoolClient
+ TokenEndpoint:
+ Value: !Sub 'https://mcp-oauth-example-${AWS::AccountId}-${sls:stage}.auth.${AWS::Region}.amazoncognito.com/oauth2/token'
+ Issuer:
+ Value: !Sub 'https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}'
diff --git a/mcp/oauth-cognito/src/server.mjs b/mcp/oauth-cognito/src/server.mjs
new file mode 100644
index 000000000..25fb76ff5
--- /dev/null
+++ b/mcp/oauth-cognito/src/server.mjs
@@ -0,0 +1,82 @@
+/**
+ * A standard MCP server, built with the official MCP TypeScript SDK. There is
+ * nothing Lambda-specific and nothing Serverless Framework-specific in here:
+ * the `mcp` property packages this module with an entry that hands its default
+ * export a web-standard request to answer.
+ *
+ * The default export must be what `createMcpHandler()` returns, and the module
+ * must be ESM.
+ */
+import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'
+import { z } from 'zod'
+
+export default createMcpHandler(() => {
+ const server = new McpServer({ name: 'demo', version: '1.0.0' })
+
+ // An instant tool. zod schemas sit next to the implementation, and returning
+ // structuredContent alongside the text lets clients consume a typed result.
+ server.registerTool(
+ 'add',
+ {
+ description: 'Add two numbers',
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
+ // .finite() with a message: an overflow to Infinity fails output
+ // validation either way, but zod's default text for it ("expected
+ // number, received number") is undiagnosable.
+ outputSchema: z.object({
+ sum: z.number().finite('a + b overflowed the double range'),
+ }),
+ },
+ async ({ a, b }) => {
+ const output = { sum: a + b }
+ return {
+ content: [{ type: 'text', text: String(output.sum) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ // A slow tool that reports progress. Each notification is a write on the
+ // response stream, so the client sees the work advancing instead of waiting —
+ // and the connection never goes quiet long enough to be cut. Emit progress
+ // from any tool that takes more than a moment; a tool running longer than
+ // ~300 seconds has to, whatever its `timeout` is.
+ server.registerTool(
+ 'slow_report',
+ {
+ description: 'Generate a report, reporting progress along the way',
+ // Bounded on purpose: nothing else stops steps: 9e15, and the only
+ // backstop past the schema is the function timeout.
+ inputSchema: z.object({ steps: z.number().int().min(1).max(60).default(5) }),
+ },
+ async ({ steps }, ctx) => {
+ // Clients that want progress send a token with the call; ones that do not
+ // get the same result without the notifications.
+ const progressToken = ctx.mcpReq._meta?.progressToken
+ for (let step = 1; step <= steps; step++) {
+ await new Promise((resolve) => setTimeout(resolve, 800))
+ if (progressToken !== undefined) {
+ await ctx.mcpReq.notify({
+ method: 'notifications/progress',
+ params: {
+ progressToken,
+ progress: step,
+ total: steps,
+ message: `step ${step}`,
+ },
+ })
+ }
+ }
+ return {
+ content: [
+ {
+ type: 'text',
+ text: `completed ${steps} ${steps === 1 ? 'step' : 'steps'}`,
+ },
+ ],
+ }
+ },
+ )
+
+ return server
+})
diff --git a/mcp/oauth-in-module/README.md b/mcp/oauth-in-module/README.md
new file mode 100644
index 000000000..430e4bdca
--- /dev/null
+++ b/mcp/oauth-in-module/README.md
@@ -0,0 +1,198 @@
+
+
+# MCP Server Verifying OAuth In-Module
+
+The in-module member of the [MCP examples](../README.md). The [Framework never verifies tokens](https://www.serverless.com/framework/docs/providers/aws/guide/mcp#authentication); the gateway-enforced siblings ([oauth-cognito](../oauth-cognito), [oauth-authorizer](../oauth-authorizer)) reject before the function runs, and what they cannot give you is the MCP specification's own semantics — those live where the server lives. This example puts enforcement **inside the module**, using the MCP SDK's own `requireBearerAuth`:
+
+- a rejected request gets the spec's `401` with a `WWW-Authenticate` challenge — not API Gateway's bare `{"message":"Unauthorized"}`
+- a token that verifies but lacks a required scope gets `403 insufficient_scope` — a distinction no gateway shape draws
+- the verified identity reaches every tool as `ctx.http.authInfo`, and the `whoami` tool here returns it, so you can see the mechanism end to end
+
+The shape of the module is the whole trick: build the handler as usual, build the gate, and default-export a `fetch` that runs the gate first. That still satisfies the module contract — an object exposing a web-standard `fetch` — so nothing about deployment changes:
+
+```js
+const gate = requireBearerAuth({
+ verifier: {
+ async verifyAccessToken(token) {
+ /* your JWKS check */
+ },
+ },
+ requiredScopes: REQUIRED_SCOPES,
+});
+
+export default {
+ async fetch(request, options) {
+ const auth = await gate(request);
+ if (auth instanceof Response) return auth;
+ return handler.fetch(request, { ...options, authInfo: auth });
+ },
+};
+```
+
+Verification itself is yours. Here it is a JWKS check with [`jose`](https://github.com/panva/jose) against the issuer in `MCP_ISSUER` — swap in token introspection or your library of choice; whatever verifies, return the SDK's `AuthInfo` shape with `expiresAt` populated (the gate rejects tokens without one).
+
+In `serverless.yml`, the server carries **no `authorizer`** — every request reaches the function, where the module judges it — and `oauthDiscovery` publishes the discovery document that tells clients where to log in:
+
+```yaml
+mcp:
+ servers:
+ demo:
+ server: src/server.mjs
+ oauthDiscovery:
+ issuer: ${env:MCP_ISSUER}
+ environment:
+ MCP_ISSUER: ${env:MCP_ISSUER}
+ MCP_REQUIRED_SCOPES: ${env:MCP_REQUIRED_SCOPES, ''}
+```
+
+The trade-off against a gateway authorizer is cost: every rejection here runs the full server function, where a gateway rejection stops at the authorizer — or costs no invocation at all with a Cognito pool. The strongest setup composes both: a gateway authorizer checking that a token is present and plausibly valid — cheap rejection of floods — and this gate for the judgement that needs the spec's semantics. Either sibling example's authorizer drops into this service unchanged.
+
+## One platform caveat: the challenge header arrives renamed
+
+API Gateway REST renames the `WWW-Authenticate` header in transit, so clients receive the challenge as `x-amzn-remapped-www-authenticate` — AWS-documented, not configurable. A client that reads only `WWW-Authenticate` learns nothing from it. What carries clients into the login flow anyway is the **bare `401` itself**: the official SDK client starts its OAuth flow on any `401` and then probes the well-known discovery paths (path-aware, with a root fallback), and clients that probe by convention — Claude Code is one — go straight to the well-known paths and never read response headers at all. Neither class depends on the renamed header, so the rename itself costs them nothing — but whether their probes then find the document `oauthDiscovery` publishes is decided by the endpoint's shape: the well-known probes look under the origin root, which the raw `execute-api` URL this README tests against cannot satisfy; the [oauth-cognito README](../oauth-cognito/README.md#interactive-clients-need-a-custom-domain) covers the root-mapped custom domain that fixes it. Machine-to-machine callers, which already hold their tokens, never probe at all.
+
+## Setup
+
+Any OIDC issuer whose tokens carry `exp` and (for the scope check) `scope` works. The quickest self-contained path: deploy the [oauth-cognito](../oauth-cognito) sibling first and borrow its pool as the issuer — its stack outputs carry everything, and its README's token walkthrough mints the tokens you'll test with:
+
+```bash
+STACK=mcp-oauth-cognito-dev
+export MCP_ISSUER=$(aws cloudformation describe-stacks --stack-name "$STACK" \
+ --query "Stacks[0].Outputs[?OutputKey=='Issuer'].OutputValue" --output text)
+export MCP_REQUIRED_SCOPES="mcp/invoke"
+```
+
+## Deployment
+
+```bash
+npm install
+serverless deploy
+```
+
+```
+mcp: demo → https://xyz789ghi.execute-api.us-east-1.amazonaws.com/dev/demo/mcp
+```
+
+The deploy also prints a warning that the discovery document lives at the stage URL, where interactive clients cannot discover it. That is expected on this example's default endpoint — the [platform caveat](#one-platform-caveat-the-challenge-header-arrives-renamed) above covers the endpoint-shape question, and the machine-to-machine callers this README tests with never read the document at all. When a custom domain does front the service, declare it under `provider.domain`, or point `mcp.servers.demo.oauthDiscovery.publicUrl` at a domain managed outside this service — either aims the advertisement at it.
+
+Set `ENDPOINT` to that URL for everything below.
+
+## Testing
+
+### The rejections carry the spec's shape
+
+No token — a `401` whose challenge (under the remapped name) names the error:
+
+```bash
+curl -sS -o /dev/null -D- -X POST "$ENDPOINT" \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream' \
+ -H 'mcp-method: tools/list' \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' \
+ | grep -i 'http/\|www-authenticate'
+```
+
+```
+HTTP/2 401
+x-amzn-remapped-www-authenticate: Bearer error="invalid_token", error_description="Missing Authorization header", scope="mcp/invoke"
+```
+
+A garbage token is judged by your verifier and rejected the same way, `error="invalid_token", error_description="Token verification failed"`. And unlike the gateway-enforced siblings, each rejection shows up in `serverless logs -f demo` as an invocation record — `START`/`END`/`REPORT` lines with a billed duration, not a log line naming the rejection. The function ran; that is the cost side of the trade-off, visible.
+
+The discovery document reads without a token, as always:
+
+```bash
+curl -sS "https://xyz789ghi.execute-api.us-east-1.amazonaws.com/dev/.well-known/oauth-protected-resource/demo/mcp"
+```
+
+```json
+{
+ "resource": "https://xyz789ghi.execute-api.us-east-1.amazonaws.com/dev/demo/mcp",
+ "authorization_servers": [
+ "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX"
+ ],
+ "bearer_methods_supported": ["header"]
+}
+```
+
+### A valid token reaches the tools — identity attached
+
+Mint `TOKEN` per the [oauth-cognito README](../oauth-cognito/README.md#mint-a-token-and-call-a-tool) (or from your own issuer), then ask the server who is calling:
+
+```bash
+curl -sS -X POST "$ENDPOINT" \
+ -H 'content-type: application/json' \
+ -H 'accept: application/json, text/event-stream' \
+ -H 'mcp-method: tools/call' -H 'mcp-name: whoami' \
+ -H "authorization: Bearer $TOKEN" \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whoami","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
+```
+
+```json
+{
+ "result": {
+ "content": [
+ {
+ "type": "text",
+ "text": "{\"clientId\":\"abc123def456ghi789jkl0\",\"scopes\":[\"mcp/invoke\"]}"
+ }
+ ],
+ "structuredContent": {
+ "clientId": "abc123def456ghi789jkl0",
+ "scopes": ["mcp/invoke"]
+ },
+ "resultType": "complete"
+ },
+ "jsonrpc": "2.0",
+ "id": 1
+}
+```
+
+(Trimmed — the live response also carries a `_meta` block naming the server.)
+
+That `clientId` is the caller's — verified by your code, attached by the gate, consumed by the tool. To see the scope path reject, require a scope your tokens do not carry (`MCP_REQUIRED_SCOPES="mcp/admin" serverless deploy`) and repeat the call: the same valid token now gets `403` with `error="insufficient_scope", error_description="Insufficient scope", scope="mcp/admin"` on the remapped challenge header. The service keeps requiring `mcp/admin` — and rejecting your tokens — until you restore it, so redeploy with a plain `serverless deploy` (the `mcp/invoke` export from Setup still stands) before moving on.
+
+The [hub README](../README.md) has the official-client script (pass the token as `BEARER`) and the capability matrix that applies to every example in the family.
+
+## Claude Code
+
+The same token puts **Claude Code** on this endpoint — no custom domain, no login flow, because the header carries what the login flow would have obtained. The `--` is required after `--header`, which takes multiple values and would otherwise swallow the name and URL that follow it; commands without `--header` need no separator:
+
+```bash
+claude mcp add --transport http --header "Authorization: Bearer $TOKEN" -- demo "$ENDPOINT"
+claude mcp list
+```
+
+```
+demo: https://xyz789ghi.execute-api.us-east-1.amazonaws.com/dev/demo/mcp (HTTP) - ✔ Connected
+```
+
+A headless call closes the loop — the phrasing of the reply is the model's, but the identity it returns is the one your verifier attached, retrieved by a real MCP client:
+
+```bash
+claude -p "call the whoami tool from the demo MCP server and show the raw result" --allowedTools "mcp__demo__whoami"
+```
+
+```
+{"clientId":"abc123def456ghi789jkl0","scopes":["mcp/invoke"]}
+```
+
+A pasted header never refreshes, so calls start answering the gate's `401` when the token expires — re-add with a fresh one. `claude mcp remove demo` deregisters it. The browser-login alternative is the [oauth-cognito walkthrough](../oauth-cognito/README.md#interactive-clients-need-a-custom-domain), and it applies here the same way once a root-mapped domain fronts the service.
+
+## Clean up
+
+```bash
+serverless remove
+```
+
+If you stood up the [oauth-cognito](../oauth-cognito) sibling just to serve as this example's issuer, it needs its own `serverless remove`, run there.
diff --git a/mcp/oauth-in-module/package-lock.json b/mcp/oauth-in-module/package-lock.json
new file mode 100644
index 000000000..bcdd46b61
--- /dev/null
+++ b/mcp/oauth-in-module/package-lock.json
@@ -0,0 +1,60 @@
+{
+ "name": "mcp-oauth-in-module",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mcp-oauth-in-module",
+ "version": "1.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "jose": "^6.1.0",
+ "zod": "^4.2.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/core": "2.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/jose": {
+ "version": "6.2.7",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.7.tgz",
+ "integrity": "sha512-hq1OB1bALKfydZNoViyg6hPVGV4i93ny9Op+n4zP5RSf7SCZEXa/TsG2O3IEr7+WlHRTPnpqDmHfMH6qXAD60w==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/mcp/oauth-in-module/package.json b/mcp/oauth-in-module/package.json
new file mode 100644
index 000000000..8e4b95409
--- /dev/null
+++ b/mcp/oauth-in-module/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "mcp-oauth-in-module",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "MCP server deployed with the Serverless Framework",
+ "dependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "jose": "^6.1.0",
+ "zod": "^4.2.0"
+ }
+}
diff --git a/mcp/oauth-in-module/serverless.yml b/mcp/oauth-in-module/serverless.yml
new file mode 100644
index 000000000..cb168cf1e
--- /dev/null
+++ b/mcp/oauth-in-module/serverless.yml
@@ -0,0 +1,44 @@
+# An MCP server that verifies bearer tokens INSIDE the module, with the MCP
+# SDK's own `requireBearerAuth` gate wrapped around the handler — the path to
+# the specification's full auth semantics: the 401 carries a WWW-Authenticate
+# challenge, a token missing a required scope is a 403 insufficient_scope, and
+# the verified identity reaches your tools as ctx.http.authInfo.
+#
+# The Framework's part is advertisement and delivery only: `oauthDiscovery`
+# publishes the protected-resource document, and the endpoint itself is open —
+# every request reaches the function, where src/server.mjs judges it.
+service: mcp-oauth-in-module
+frameworkVersion: '4'
+
+provider:
+ name: aws
+ runtime: nodejs24.x # the MCP SDK requires Node.js 20 or newer
+ # Lets a response stream stay quiet for roughly 5 minutes; the edge-optimized
+ # default ends one quiet for roughly 30 seconds.
+ endpointType: REGIONAL
+
+mcp:
+ servers:
+ demo:
+ server: src/server.mjs
+
+ # No `authorizer:` — deliberately. Rejection happens in the module, which
+ # is what buys the spec-shaped responses, paid for with one invocation
+ # per rejected request. A gateway authorizer in front of this module is
+ # the hardened combination: cheap rejection of floods at API Gateway,
+ # the spec's judgement in here. Either sibling example's authorizer
+ # drops in unchanged.
+
+ # Advertisement: the RFC 9728 protected-resource document at
+ # /.well-known/oauth-protected-resource/demo/mcp, so clients can find
+ # the authorization server. It enforces nothing — the module does.
+ oauthDiscovery:
+ issuer: ${env:MCP_ISSUER}
+
+ environment:
+ # The OIDC issuer whose tokens the gate accepts; its JWKS is fetched
+ # from /.well-known/jwks.json.
+ MCP_ISSUER: ${env:MCP_ISSUER}
+ # Space-separated scopes every token must carry. Leave unset to accept
+ # any token the issuer signed.
+ MCP_REQUIRED_SCOPES: ${env:MCP_REQUIRED_SCOPES, ''}
diff --git a/mcp/oauth-in-module/src/server.mjs b/mcp/oauth-in-module/src/server.mjs
new file mode 100644
index 000000000..2d14a9094
--- /dev/null
+++ b/mcp/oauth-in-module/src/server.mjs
@@ -0,0 +1,139 @@
+/**
+ * An MCP server that verifies bearer tokens itself, with the SDK's own
+ * `requireBearerAuth` gate wrapped around the handler. The default export is
+ * no longer bare `createMcpHandler()` output — it is a small object whose
+ * `fetch` runs the gate first — but it still satisfies the module contract
+ * (an object exposing a web-standard `fetch`), so nothing about deployment
+ * changes.
+ *
+ * The gate owns the spec's semantics: a rejected request gets a `401` with a
+ * `WWW-Authenticate` challenge (or a `403 insufficient_scope` when a required
+ * scope is missing), and an accepted one hands the verified identity to every
+ * tool as `ctx.http.authInfo`. Verification itself is yours — here, a JWKS
+ * check with `jose` against the issuer in `MCP_ISSUER`.
+ */
+import {
+ createMcpHandler,
+ McpServer,
+ OAuthError,
+ OAuthErrorCode,
+ requireBearerAuth,
+} from '@modelcontextprotocol/server'
+import { createRemoteJWKSet, jwtVerify } from 'jose'
+import { z } from 'zod'
+
+const ISSUER = process.env.MCP_ISSUER
+const REQUIRED_SCOPES = (process.env.MCP_REQUIRED_SCOPES ?? '')
+ .split(' ')
+ .filter(Boolean)
+
+// Module scope on purpose: one remote key set per execution environment,
+// fetched from the issuer on first use and cached across invocations.
+const jwks = createRemoteJWKSet(
+ new URL(
+ '.well-known/jwks.json',
+ ISSUER.endsWith('/') ? ISSUER : `${ISSUER}/`,
+ ),
+)
+
+const handler = createMcpHandler(() => {
+ const server = new McpServer({ name: 'demo', version: '1.0.0' })
+
+ // An instant tool. zod schemas sit next to the implementation, and returning
+ // structuredContent alongside the text lets clients consume a typed result.
+ server.registerTool(
+ 'add',
+ {
+ description: 'Add two numbers',
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
+ // .finite() with a message: an overflow to Infinity fails output
+ // validation either way, but zod's default text for it ("expected
+ // number, received number") is undiagnosable.
+ outputSchema: z.object({
+ sum: z.number().finite('a + b overflowed the double range'),
+ }),
+ },
+ async ({ a, b }) => {
+ const output = { sum: a + b }
+ return {
+ content: [{ type: 'text', text: String(output.sum) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ // The reason to verify in-module, demonstrated: the identity the gate
+ // attached is right there in the tool's context, so tools can act on WHO is
+ // calling — neither gateway enforcement shape can hand you this.
+ server.registerTool(
+ 'whoami',
+ {
+ description: 'Return the verified identity of the caller',
+ // Required even for a tool with no inputs: without `inputSchema` the SDK
+ // passes the context as the callback's ONLY argument.
+ inputSchema: z.object({}),
+ },
+ async (_args, ctx) => {
+ const { clientId, scopes } = ctx.http.authInfo ?? {}
+ const output = { clientId, scopes }
+ return {
+ content: [{ type: 'text', text: JSON.stringify(output) }],
+ structuredContent: output,
+ }
+ },
+ )
+
+ return server
+})
+
+const gate = requireBearerAuth({
+ // Your verification, returning the SDK's AuthInfo shape. `expiresAt` must be
+ // populated — the gate rejects tokens without one — and `jwtVerify` requiring
+ // the `exp` claim is what guarantees it is.
+ verifier: {
+ async verifyAccessToken(token) {
+ // A failed verification must be thrown as the SDK's OAuthError: that is
+ // what the gate turns into the spec's 401 invalid_token challenge — any
+ // other throw is answered as a 500 server_error instead.
+ let payload
+ try {
+ ;({ payload } = await jwtVerify(token, jwks, {
+ issuer: ISSUER,
+ requiredClaims: ['exp'],
+ }))
+ } catch {
+ // Deliberately unspecific: the reason a token failed is for your
+ // logs, not for the caller. Deliberately total, too: a JWKS fetch
+ // failure lands here and is blamed on the token — fine for an
+ // example; a production verifier may prefer to let infrastructure
+ // failures surface as server errors instead.
+ throw new OAuthError(OAuthErrorCode.InvalidToken, 'Token verification failed')
+ }
+ return {
+ token,
+ // Auth0 puts the caller's client id in `azp`, Cognito in `client_id`;
+ // `sub` is the OIDC-guaranteed fallback.
+ clientId: String(payload.client_id ?? payload.azp ?? payload.sub ?? ''),
+ scopes: String(payload.scope ?? '').split(' ').filter(Boolean),
+ expiresAt: payload.exp,
+ }
+ },
+ },
+ // A token that verifies but lacks one of these is a 403 insufficient_scope —
+ // the distinction a gateway authorizer's bare 401/403 never draws.
+ requiredScopes: REQUIRED_SCOPES,
+ // `resourceMetadataUrl` would put a resource_metadata pointer on the
+ // challenge. Omitted on purpose: this front door renames the challenge
+ // header in transit (see the README), so what reaches the discovery
+ // document is the well-known probes, not the header — and the probes only
+ // land behind a root-mapped custom domain.
+})
+
+export default {
+ async fetch(request, options) {
+ const auth = await gate(request)
+ if (auth instanceof Response) return auth
+ // Forward the second argument: it carries the pre-parsed request body.
+ return handler.fetch(request, { ...options, authInfo: auth })
+ },
+}
diff --git a/validate.js b/validate.js
index fe787de5d..5a836ee51 100644
--- a/validate.js
+++ b/validate.js
@@ -5,7 +5,7 @@ const path = require('path');
const ROOT = __dirname;
const REQUIRED = ['title', 'description', 'framework', 'platform', 'language', 'authorLink', 'authorName', 'authorAvatar'];
-const SKIP_DIRS = new Set(['node_modules', '.git', '.github', 'docs', 'images', '.serverless']);
+const SKIP_DIRS = new Set(['node_modules', '.git', '.github', '.claude', 'docs', 'images', '.serverless']);
// An example root is the shallowest directory containing serverless.yml/yaml.
// Do not recurse below an example root (multi-service examples validate once, at the root).
@@ -52,6 +52,41 @@ for (const dir of findExampleDirs(ROOT)) {
}
}
+// The mcp/custom examples all serve one canonical MCP server and ship
+// one shared test client. The canonical copies live at mcp/custom/
+// (client.mjs, server.mjs); every example carries an identical copy so each
+// directory stays self-contained. Edit the canonical, then re-copy.
+const SHARED_FILES = [
+ ['mcp/custom/client.mjs', [
+ 'mcp/custom/rest-api/client.mjs',
+ 'mcp/custom/function-url/client.mjs',
+ 'mcp/custom/hono/client.mjs',
+ 'mcp/custom/express-web-adapter/client.mjs',
+ 'mcp/custom/fastify-container/client.mjs',
+ 'aws-bedrock-agentcore/javascript/mcp-server/client.mjs',
+ ]],
+ ['mcp/custom/server.mjs', [
+ 'mcp/custom/rest-api/src/server.mjs',
+ 'mcp/custom/function-url/src/server.mjs',
+ 'mcp/custom/hono/src/server.mjs',
+ 'mcp/custom/express-web-adapter/src/server.mjs',
+ 'mcp/custom/fastify-container/src/server.mjs',
+ 'aws-bedrock-agentcore/javascript/mcp-server/src/server.mjs',
+ ]],
+];
+for (const [canonical, copies] of SHARED_FILES) {
+ const canonicalPath = path.join(ROOT, canonical);
+ if (!fs.existsSync(canonicalPath)) { errors.push(`${canonical}: canonical shared file missing`); continue; }
+ const want = fs.readFileSync(canonicalPath, 'utf8');
+ for (const copy of copies) {
+ const copyPath = path.join(ROOT, copy);
+ if (!fs.existsSync(copyPath)) { errors.push(`${copy}: missing copy of ${canonical}`); continue; }
+ if (fs.readFileSync(copyPath, 'utf8') !== want) {
+ errors.push(`${copy}: differs from canonical ${canonical} — edit the canonical and re-copy`);
+ }
+ }
+}
+
if (errors.length) {
console.error(`${errors.length} validation error(s):`);
errors.forEach((e) => console.error(` - ${e}`));