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}`));