diff --git a/README.md b/README.md
index c7fa36137..03fc76ff5 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/aws-mcp-servers/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/aws-mcp-servers/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/aws-mcp-servers/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/aws-mcp-servers/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 with Fastify in a Container Image (NodeJS)](https://github.com/serverless/examples/tree/v4/aws-mcp-servers/fastify-container)
Run an MCP server as a containerized Fastify app on AWS Lambda with Lambda Web Adapter - the same image runs on Fargate or anywhere else. | 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 |
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..8ede92bfe
--- /dev/null
+++ b/aws-bedrock-agentcore/javascript/mcp-server/client.mjs
@@ -0,0 +1,335 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the aws-mcp-servers 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, progressToken: 'pt' }, _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))
+ return 'input_required -> refunded o-1 / refund cancelled'
+})
+
+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, progressToken: 'long' }, _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..fdf1476e6
--- /dev/null
+++ b/aws-bedrock-agentcore/javascript/mcp-server/src/server.mjs
@@ -0,0 +1,193 @@
+/**
+ * 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 aws-mcp-servers/ 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,
+ 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), progressToken: z.string().optional() }),
+ },
+ async ({ steps, progressToken }, ctx) => {
+ 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() }),
+ )
+ 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/aws-mcp-servers/README.md b/aws-mcp-servers/README.md
new file mode 100644
index 000000000..4ffea22de
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/agentcore-gateway/README.md b/aws-mcp-servers/agentcore-gateway/README.md
new file mode 100644
index 000000000..0b8a80e0e
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/agentcore-runtime/README.md b/aws-mcp-servers/agentcore-runtime/README.md
new file mode 100644
index 000000000..0c3c8bf05
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/client.mjs b/aws-mcp-servers/client.mjs
new file mode 100644
index 000000000..8ede92bfe
--- /dev/null
+++ b/aws-mcp-servers/client.mjs
@@ -0,0 +1,335 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the aws-mcp-servers 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, progressToken: 'pt' }, _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))
+ return 'input_required -> refunded o-1 / refund cancelled'
+})
+
+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, progressToken: 'long' }, _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-mcp-servers/express-web-adapter/README.md b/aws-mcp-servers/express-web-adapter/README.md
new file mode 100644
index 000000000..0ca2bc596
--- /dev/null
+++ b/aws-mcp-servers/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 [aws-mcp-servers](../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/aws-mcp-servers/express-web-adapter/client.mjs b/aws-mcp-servers/express-web-adapter/client.mjs
new file mode 100644
index 000000000..8ede92bfe
--- /dev/null
+++ b/aws-mcp-servers/express-web-adapter/client.mjs
@@ -0,0 +1,335 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the aws-mcp-servers 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, progressToken: 'pt' }, _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))
+ return 'input_required -> refunded o-1 / refund cancelled'
+})
+
+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, progressToken: 'long' }, _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-mcp-servers/express-web-adapter/package-lock.json b/aws-mcp-servers/express-web-adapter/package-lock.json
new file mode 100644
index 000000000..4c07ded24
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/express-web-adapter/package.json b/aws-mcp-servers/express-web-adapter/package.json
new file mode 100644
index 000000000..5216406f1
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/express-web-adapter/run.sh b/aws-mcp-servers/express-web-adapter/run.sh
new file mode 100755
index 000000000..ca91880bc
--- /dev/null
+++ b/aws-mcp-servers/express-web-adapter/run.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+exec node src/index.mjs
diff --git a/aws-mcp-servers/express-web-adapter/serverless.yml b/aws-mcp-servers/express-web-adapter/serverless.yml
new file mode 100644
index 000000000..c2c455041
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/express-web-adapter/src/authorizer.mjs b/aws-mcp-servers/express-web-adapter/src/authorizer.mjs
new file mode 100644
index 000000000..6e451fced
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/express-web-adapter/src/index.mjs b/aws-mcp-servers/express-web-adapter/src/index.mjs
new file mode 100644
index 000000000..8187e52df
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/express-web-adapter/src/server.mjs b/aws-mcp-servers/express-web-adapter/src/server.mjs
new file mode 100644
index 000000000..fdf1476e6
--- /dev/null
+++ b/aws-mcp-servers/express-web-adapter/src/server.mjs
@@ -0,0 +1,193 @@
+/**
+ * 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 aws-mcp-servers/ 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,
+ 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), progressToken: z.string().optional() }),
+ },
+ async ({ steps, progressToken }, ctx) => {
+ 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() }),
+ )
+ 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-mcp-servers/fastify-container/Dockerfile b/aws-mcp-servers/fastify-container/Dockerfile
new file mode 100644
index 000000000..b9d26fa5d
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/fastify-container/README.md b/aws-mcp-servers/fastify-container/README.md
new file mode 100644
index 000000000..7e7f5717e
--- /dev/null
+++ b/aws-mcp-servers/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 [aws-mcp-servers](../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/aws-mcp-servers/fastify-container/client.mjs b/aws-mcp-servers/fastify-container/client.mjs
new file mode 100644
index 000000000..8ede92bfe
--- /dev/null
+++ b/aws-mcp-servers/fastify-container/client.mjs
@@ -0,0 +1,335 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the aws-mcp-servers 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, progressToken: 'pt' }, _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))
+ return 'input_required -> refunded o-1 / refund cancelled'
+})
+
+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, progressToken: 'long' }, _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-mcp-servers/fastify-container/package-lock.json b/aws-mcp-servers/fastify-container/package-lock.json
new file mode 100644
index 000000000..240a7e706
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/fastify-container/package.json b/aws-mcp-servers/fastify-container/package.json
new file mode 100644
index 000000000..2ad00f00a
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/fastify-container/serverless.yml b/aws-mcp-servers/fastify-container/serverless.yml
new file mode 100644
index 000000000..dc2ae3c5a
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/fastify-container/src/index.mjs b/aws-mcp-servers/fastify-container/src/index.mjs
new file mode 100644
index 000000000..d67c8e64b
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/fastify-container/src/server.mjs b/aws-mcp-servers/fastify-container/src/server.mjs
new file mode 100644
index 000000000..fdf1476e6
--- /dev/null
+++ b/aws-mcp-servers/fastify-container/src/server.mjs
@@ -0,0 +1,193 @@
+/**
+ * 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 aws-mcp-servers/ 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,
+ 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), progressToken: z.string().optional() }),
+ },
+ async ({ steps, progressToken }, ctx) => {
+ 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() }),
+ )
+ 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-mcp-servers/function-url/README.md b/aws-mcp-servers/function-url/README.md
new file mode 100644
index 000000000..0efa868ec
--- /dev/null
+++ b/aws-mcp-servers/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 [aws-mcp-servers](../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/aws-mcp-servers/function-url/client.mjs b/aws-mcp-servers/function-url/client.mjs
new file mode 100644
index 000000000..8ede92bfe
--- /dev/null
+++ b/aws-mcp-servers/function-url/client.mjs
@@ -0,0 +1,335 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the aws-mcp-servers 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, progressToken: 'pt' }, _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))
+ return 'input_required -> refunded o-1 / refund cancelled'
+})
+
+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, progressToken: 'long' }, _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-mcp-servers/function-url/package-lock.json b/aws-mcp-servers/function-url/package-lock.json
new file mode 100644
index 000000000..2ae0e613b
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/function-url/package.json b/aws-mcp-servers/function-url/package.json
new file mode 100644
index 000000000..0aa958249
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/function-url/serverless.yml b/aws-mcp-servers/function-url/serverless.yml
new file mode 100644
index 000000000..508bda4e3
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/function-url/src/lambda.mjs b/aws-mcp-servers/function-url/src/lambda.mjs
new file mode 100644
index 000000000..b15e1c686
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/function-url/src/server.mjs b/aws-mcp-servers/function-url/src/server.mjs
new file mode 100644
index 000000000..fdf1476e6
--- /dev/null
+++ b/aws-mcp-servers/function-url/src/server.mjs
@@ -0,0 +1,193 @@
+/**
+ * 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 aws-mcp-servers/ 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,
+ 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), progressToken: z.string().optional() }),
+ },
+ async ({ steps, progressToken }, ctx) => {
+ 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() }),
+ )
+ 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-mcp-servers/hono/README.md b/aws-mcp-servers/hono/README.md
new file mode 100644
index 000000000..d883fc4e5
--- /dev/null
+++ b/aws-mcp-servers/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 [aws-mcp-servers](../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/aws-mcp-servers/hono/client.mjs b/aws-mcp-servers/hono/client.mjs
new file mode 100644
index 000000000..8ede92bfe
--- /dev/null
+++ b/aws-mcp-servers/hono/client.mjs
@@ -0,0 +1,335 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the aws-mcp-servers 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, progressToken: 'pt' }, _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))
+ return 'input_required -> refunded o-1 / refund cancelled'
+})
+
+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, progressToken: 'long' }, _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-mcp-servers/hono/package-lock.json b/aws-mcp-servers/hono/package-lock.json
new file mode 100644
index 000000000..5844571a1
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/hono/package.json b/aws-mcp-servers/hono/package.json
new file mode 100644
index 000000000..267b19683
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/hono/serverless.yml b/aws-mcp-servers/hono/serverless.yml
new file mode 100644
index 000000000..9e1906861
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/hono/src/lambda.mjs b/aws-mcp-servers/hono/src/lambda.mjs
new file mode 100644
index 000000000..76b35e920
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/hono/src/server.mjs b/aws-mcp-servers/hono/src/server.mjs
new file mode 100644
index 000000000..fdf1476e6
--- /dev/null
+++ b/aws-mcp-servers/hono/src/server.mjs
@@ -0,0 +1,193 @@
+/**
+ * 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 aws-mcp-servers/ 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,
+ 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), progressToken: z.string().optional() }),
+ },
+ async ({ steps, progressToken }, ctx) => {
+ 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() }),
+ )
+ 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-mcp-servers/rest-api/README.md b/aws-mcp-servers/rest-api/README.md
new file mode 100644
index 000000000..3cdc18ffa
--- /dev/null
+++ b/aws-mcp-servers/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 [aws-mcp-servers](../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/aws-mcp-servers/rest-api/client.mjs b/aws-mcp-servers/rest-api/client.mjs
new file mode 100644
index 000000000..8ede92bfe
--- /dev/null
+++ b/aws-mcp-servers/rest-api/client.mjs
@@ -0,0 +1,335 @@
+#!/usr/bin/env node
+/**
+ * Shared test client for the aws-mcp-servers 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, progressToken: 'pt' }, _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))
+ return 'input_required -> refunded o-1 / refund cancelled'
+})
+
+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, progressToken: 'long' }, _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-mcp-servers/rest-api/package-lock.json b/aws-mcp-servers/rest-api/package-lock.json
new file mode 100644
index 000000000..bb281ae2a
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/rest-api/package.json b/aws-mcp-servers/rest-api/package.json
new file mode 100644
index 000000000..6e6e56b5f
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/rest-api/serverless.yml b/aws-mcp-servers/rest-api/serverless.yml
new file mode 100644
index 000000000..db1b45da0
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/rest-api/src/authorizer.mjs b/aws-mcp-servers/rest-api/src/authorizer.mjs
new file mode 100644
index 000000000..6e451fced
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/rest-api/src/lambda.mjs b/aws-mcp-servers/rest-api/src/lambda.mjs
new file mode 100644
index 000000000..372c1d1f5
--- /dev/null
+++ b/aws-mcp-servers/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/aws-mcp-servers/rest-api/src/server.mjs b/aws-mcp-servers/rest-api/src/server.mjs
new file mode 100644
index 000000000..fdf1476e6
--- /dev/null
+++ b/aws-mcp-servers/rest-api/src/server.mjs
@@ -0,0 +1,193 @@
+/**
+ * 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 aws-mcp-servers/ 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,
+ 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), progressToken: z.string().optional() }),
+ },
+ async ({ steps, progressToken }, ctx) => {
+ 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() }),
+ )
+ 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-mcp-servers/server.mjs b/aws-mcp-servers/server.mjs
new file mode 100644
index 000000000..fdf1476e6
--- /dev/null
+++ b/aws-mcp-servers/server.mjs
@@ -0,0 +1,193 @@
+/**
+ * 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 aws-mcp-servers/ 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,
+ 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), progressToken: z.string().optional() }),
+ },
+ async ({ steps, progressToken }, ctx) => {
+ 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() }),
+ )
+ 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/examples.json b/examples.json
index 2e9bb402a..111d0d3e1 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": "aws-mcp-servers-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/aws-mcp-servers/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": "aws-mcp-servers-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/aws-mcp-servers/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": "aws-mcp-servers-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/aws-mcp-servers/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": "aws-mcp-servers-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/aws-mcp-servers/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 with Fastify in a Container Image (NodeJS)",
+ "name": "aws-mcp-servers-fastify-container",
+ "description": "Run an MCP server as a containerized Fastify app on AWS Lambda with Lambda Web Adapter - the same image runs on Fargate or anywhere else.",
+ "githubUrl": "https://github.com/serverless/examples/tree/v4/aws-mcp-servers/fastify-container",
+ "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",
diff --git a/validate.js b/validate.js
index fe787de5d..a22f6f438 100644
--- a/validate.js
+++ b/validate.js
@@ -52,6 +52,41 @@ for (const dir of findExampleDirs(ROOT)) {
}
}
+// The aws-mcp-servers examples all serve one canonical MCP server and ship
+// one shared test client. The canonical copies live at aws-mcp-servers/
+// (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 = [
+ ['aws-mcp-servers/client.mjs', [
+ 'aws-mcp-servers/rest-api/client.mjs',
+ 'aws-mcp-servers/function-url/client.mjs',
+ 'aws-mcp-servers/hono/client.mjs',
+ 'aws-mcp-servers/express-web-adapter/client.mjs',
+ 'aws-mcp-servers/fastify-container/client.mjs',
+ 'aws-bedrock-agentcore/javascript/mcp-server/client.mjs',
+ ]],
+ ['aws-mcp-servers/server.mjs', [
+ 'aws-mcp-servers/rest-api/src/server.mjs',
+ 'aws-mcp-servers/function-url/src/server.mjs',
+ 'aws-mcp-servers/hono/src/server.mjs',
+ 'aws-mcp-servers/express-web-adapter/src/server.mjs',
+ 'aws-mcp-servers/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}`));